one_time_password 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 341d54de2197c31d888d896395ae79c514d3d7899a27637c66d8ec9899e8cf97
4
+ data.tar.gz: 1f6346bd6e38f32a52394c0ccb37e3058fcd956ef3878ef69f214bcc67ac617c
5
+ SHA512:
6
+ metadata.gz: 2c76a03d196b10156ca63236f4fe835f3b63223bcadf63233e4b68bb2c7aebb196e4cb34ab99235a4418dbe7418c90aa3e31a3760e23df766c54971b9b24dc2b
7
+ data.tar.gz: 5750577c5bc3fb5c3bd13b1d39302da51e60867bf154168e69bc360c5907468d6c81fa2ac55faaceb4d4eea00c36f2f7a90dd3c99ffdca1c9de1a11efe999d2e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # OneTimePassword
2
+
3
+ This Gem can be used to create 2FA (Two-Factor Authentication) function, email address verification function for member registration and etc in Ruby on Rails.
4
+
5
+
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "one_time_password"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install one_time_password
22
+ ```
23
+
24
+
25
+
26
+ ## Usage
27
+
28
+ ### Run command for an installation.
29
+
30
+ ```bash
31
+ bundle exec rails g one_time_password:install
32
+ ```
33
+
34
+ The following events will take place when using the install generator:
35
+ - An initializer file will be created at `config/initializers/one_time_password.rb`
36
+ - A migration file will be created at `db/migrate/xxxxxxxxxxxxxx_create_one_time_authentication.rb`
37
+ - A model file will be created at `app/models/one_time_authentication.rb`
38
+
39
+ And run migration.
40
+
41
+ ```bash
42
+ bundle exec rails db:migrate
43
+ ```
44
+
45
+ ### Rewrite `FUNCTION_NAMES` and `CONTEXTS` in initializer settings.
46
+
47
+ Configuration in `config/initializers/one_time_password.rb`.
48
+
49
+ `FUNCTION_NAMES`: Using function_name in OneTimeAuthentication Model enum.
50
+
51
+
52
+ Hash, one of `CONTEXTS`:
53
+ | | |
54
+ | --- | --- |
55
+ | function_name (Symbol) | Name each function. |
56
+ | version (Integer) | Version each function_name. |
57
+ | expires_in (ActiveSupport::Duration) | Password validity time. |
58
+ | max_authenticate_password_count (Integer) | Number of times user can enter password each generated password. |
59
+ | password_length (Integer) | Password length. At 6, for example, the password would be 123456. |
60
+ | password_failed_limit (Integer)<br>password_failed_period (ActiveSupport::Duration) | If you try to authenticate with the wrong password a password_failed_limit times within the time set by password_failed_period, you will not be able to generate a new password. |
61
+ | | |
62
+
63
+ ### See example and its sequence diagram
64
+ [here](#example-and-its-sequence-diagram)
65
+
66
+ ### `OneTimePassword::OneTimeAuthentication`'s methods.
67
+
68
+ For more information, see the [implementation of OneTimePassword :: OneTimeAuthenticationModel](https://github.com/yosipy/one_time_password/blob/main/lib/one_time_password/one_time_authentication_model.rb).
69
+
70
+
71
+
72
+ ## Example and its sequence diagram
73
+
74
+ See [sign up exsample](https://github.com/yosipy/one_time_password/blob/main/spec/dummy/app/controllers/test_users_controller.rb).
75
+
76
+ Sequence diagram.
77
+
78
+ ![sequence diagram image](document/sequence_diagram/sequencediagram.png)
79
+
80
+
81
+
82
+ <!-- ## Contributing
83
+ Contribution directions go here. -->
84
+
85
+
86
+
87
+ ## License
88
+ 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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,59 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module OneTimePassword
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ include ::Rails::Generators::Migration
7
+
8
+ class_option :warning_over_write, type: :boolean, default: false,
9
+ desc: "Orver write generator files."
10
+
11
+ def self.next_migration_number(dirname)
12
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
13
+ end
14
+
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ def create_initializer_file
18
+ template = 'one_time_password'
19
+ file_path = "config/initializers/#{template}.rb"
20
+
21
+ if !File.exist?(file_path) || options[:warning_over_write]
22
+ template(file_path, File.expand_path(file_path))
23
+ else
24
+ ::Kernel.warn "Initializers already exists: #{template}"
25
+ end
26
+ end
27
+
28
+ def create_migration_file
29
+ template = 'create_one_time_authentication'
30
+ file_dir = 'db/migrate'
31
+
32
+ if !self.class.migration_exists?(File.expand_path(file_dir), template) || options[:warning_over_write]
33
+ migration_template(
34
+ "#{file_dir}/#{template}.rb.erb",
35
+ "#{File.expand_path(file_dir)}/#{template}.rb",
36
+ migration_version: migration_version
37
+ )
38
+ else
39
+ ::Kernel.warn "Migration already exists: #{template}"
40
+ end
41
+ end
42
+
43
+ def create_model_file
44
+ template = 'one_time_authentication'
45
+ file_path = "app/models/#{template}.rb"
46
+ if !File.exist?(file_path) || options[:warning_over_write]
47
+ template(file_path, File.expand_path(file_path))
48
+ else
49
+ ::Kernel.warn "Model already exists: #{template}"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def migration_version
56
+ format("[%d.%d]", ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ class OneTimeAuthentication < ActiveRecord::Base
2
+ enum function_name: OneTimePassword::FUNCTION_NAMES
3
+
4
+ include OneTimePassword::OneTimeAuthenticationModel
5
+ end
@@ -0,0 +1,55 @@
1
+ module OneTimePassword
2
+ # Example: has sign_up, sign_in and change_email.
3
+ # Please rewrite!
4
+
5
+ # Using function_name in OneTimeAuthentication Model enum.
6
+ # ```
7
+ # # app/models/one_time_authentication.rb
8
+ # class OneTimeAuthentication < ActiveRecord::Base
9
+ # enum function_name: OneTimePassword::FUNCTION_NAMES
10
+
11
+ # include OneTimePassword::OneTimeAuthenticationModel
12
+ # end
13
+ # ```
14
+ FUNCTION_NAMES = {
15
+ sign_up: 0, sign_in: 1, change_email: 2
16
+ }
17
+
18
+ # {
19
+ # function_name (Symbol): Name each function.
20
+ # version (Integer): Version each function_name.
21
+ # expires_in (ActiveSupport::Duration): Password validity time.
22
+ # max_authenticate_password_count (Integer): Number of times user can enter password each generated password.
23
+ # password_length (Integer): Password length. At 6, for example, the password would be 123456.
24
+ # password_failed_limit (Integer) & password_failed_period (ActiveSupport::Duration):
25
+ # If you try to authenticate with the wrong password a password_failed_limit times
26
+ # within the time set by password_failed_period, you will not be able to generate a new password.
27
+ # }
28
+ CONTEXTS = [
29
+ {
30
+ function_name: :sign_up,
31
+ version: 0,
32
+ expires_in: 30.minutes,
33
+ max_authenticate_password_count: 5,
34
+ password_length: 6,
35
+ password_failed_limit: 10,
36
+ password_failed_period: 1.hour
37
+ },
38
+ {
39
+ function_name: :sign_in,
40
+ version: 0,
41
+ expires_in: 30.minutes,
42
+ max_authenticate_password_count: 5,
43
+ password_length: 10,
44
+ password_failed_limit: 10,
45
+ password_failed_period: 1.hour
46
+ },
47
+ # {
48
+ # function_name: :change_email,
49
+ # version: 0,
50
+ # expires_in: 30.minutes,
51
+ # max_authenticate_password_count: 5,
52
+ # password_length: 6
53
+ # },
54
+ ]
55
+ end
@@ -0,0 +1,18 @@
1
+ class CreateOneTimeAuthentication < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :one_time_authentications do |t|
4
+ t.integer :function_name, null: false
5
+ t.integer :version, null: false
6
+ t.string :user_key, null: false, index: true
7
+ t.string :client_token
8
+ t.integer :password_length, null: false
9
+ t.string :password_digest, null: false
10
+ t.integer :expires_seconds, null: false
11
+ t.integer :failed_count, null: false, default: 0
12
+ t.integer :max_authenticate_password_count, null: false, default: 3
13
+ t.datetime :authenticated_at
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,147 @@
1
+ module OneTimePassword
2
+ module OneTimeAuthenticationModel
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_create :set_client_token
7
+
8
+ has_secure_password
9
+
10
+ scope :unauthenticated, -> {
11
+ where(authenticated_at: nil)
12
+ }
13
+
14
+ scope :recent, -> (time_ago) {
15
+ where(created_at: Time.zone.now.ago(time_ago)...)
16
+ }
17
+ end
18
+
19
+ module ClassMethods
20
+ def find_context(function_name, version: 0)
21
+ context = OneTimePassword::CONTEXTS
22
+ .select{ |context|
23
+ context[:function_name] == function_name &&
24
+ context[:version] == version
25
+ }
26
+ .first
27
+
28
+ if context.nil?
29
+ raise ArgumentError.new('Not found context.')
30
+ elsif context[:expires_in].class != ActiveSupport::Duration
31
+ raise RuntimeError.new('Mistake OneTimePassword::CONTEXTS[:expires_in]')
32
+ elsif context[:max_authenticate_password_count].class != Integer
33
+ raise RuntimeError.new('Mistake OneTimePassword::CONTEXTS[:max_authenticate_password_count]')
34
+ elsif context[:password_length].class != Integer
35
+ raise RuntimeError.new('Mistake OneTimePassword::CONTEXTS[:password_length]')
36
+ elsif context[:password_failed_limit].class != Integer
37
+ raise RuntimeError.new('Mistake OneTimePassword::CONTEXTS[:password_failed_limit]')
38
+ elsif context[:password_failed_period].class != ActiveSupport::Duration
39
+ raise RuntimeError.new('Mistake OneTimePassword::CONTEXTS[:password_failed_period]')
40
+ end
41
+
42
+ context
43
+ end
44
+
45
+ def create_one_time_authentication(context, user_key, user_key_downcase: true)
46
+ user_key = user_key.downcase if user_key_downcase
47
+
48
+ recent_failed_authenticate_password_count =
49
+ OneTimeAuthentication
50
+ .recent_failed_authenticate_password_count(
51
+ user_key,
52
+ context[:password_failed_period]
53
+ )
54
+
55
+ if recent_failed_authenticate_password_count <= context[:password_failed_limit]
56
+ one_time_authentication = OneTimeAuthentication.new(
57
+ function_name: context[:function_name],
58
+ version: context[:version],
59
+ user_key: user_key,
60
+ password_length: context[:password_length],
61
+ expires_seconds: context[:expires_in].to_i,
62
+ max_authenticate_password_count: context[:max_authenticate_password_count],
63
+ )
64
+ one_time_authentication.set_password_and_password_length(context[:password_length])
65
+ one_time_authentication.save!
66
+ else
67
+ one_time_authentication = nil
68
+ end
69
+
70
+ one_time_authentication
71
+ end
72
+
73
+ def find_one_time_authentication(context, user_key, user_key_downcase: true)
74
+ user_key = user_key.downcase if user_key_downcase
75
+
76
+ OneTimeAuthentication
77
+ .where(function_name: context[:function_name])
78
+ .where(version: context[:version])
79
+ .where(user_key: user_key)
80
+ .last
81
+ end
82
+
83
+ def generate_random_password(length=6)
84
+ length.times.map{ SecureRandom.random_number(10) }.join
85
+ end
86
+
87
+ def recent_failed_authenticate_password_count(user_key, time_ago)
88
+ OneTimeAuthentication
89
+ .where(user_key: user_key)
90
+ .recent(time_ago)
91
+ .sum(:failed_count)
92
+ end
93
+ end
94
+
95
+ def expired?
96
+ !(self.created_at.to_f <= Time.zone.now.to_f &&
97
+ Time.zone.now.to_f <= self.created_at.to_f + self.expires_seconds.to_f)
98
+ end
99
+
100
+ def under_valid_failed_count?
101
+ self.failed_count < self.max_authenticate_password_count
102
+ end
103
+
104
+ def authenticate_one_time_client_token!(client_token)
105
+ if (self.client_token.present? &&
106
+ self.client_token == client_token)
107
+ # Refresh client_token, and return this token
108
+ new_client_token = self.set_client_token
109
+ self.save!
110
+ new_client_token
111
+ else
112
+ # Put invalid token(nil) in client_token, and return nil
113
+ self.client_token = nil
114
+ self.save!
115
+ nil
116
+ end
117
+ end
118
+
119
+ def authenticate_one_time_password!(password)
120
+ result =
121
+ if !self.expired? && self.under_valid_failed_count?
122
+ !!self.authenticate(password)
123
+ else
124
+ false
125
+ end
126
+
127
+ if result
128
+ self.authenticated_at = Time.zone.now
129
+ # Put invalid token(nil) in client_token, and return nil
130
+ self.client_token = nil
131
+ else
132
+ self.failed_count += 1
133
+ end
134
+ self.save!
135
+
136
+ result
137
+ end
138
+
139
+ def set_client_token
140
+ self.client_token = SecureRandom.urlsafe_base64
141
+ end
142
+
143
+ def set_password_and_password_length(length=6)
144
+ self.password = self.password_confirmation = OneTimeAuthentication.generate_random_password(length)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,4 @@
1
+ module OneTimePassword
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module OneTimePassword
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "one_time_password/version"
2
+ require "one_time_password/railtie"
3
+ require "one_time_password/one_time_authentication_model"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :one_time_password do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: one_time_password
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - yosipy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bcrypt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 5.1.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 5.1.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_bot_rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 6.2.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 6.2.0
83
+ description:
84
+ email:
85
+ - yosi.contact@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/generators/one_time_password/install_generator.rb
94
+ - lib/generators/one_time_password/templates/app/models/one_time_authentication.rb
95
+ - lib/generators/one_time_password/templates/config/initializers/one_time_password.rb
96
+ - lib/generators/one_time_password/templates/db/migrate/create_one_time_authentication.rb.erb
97
+ - lib/one_time_password.rb
98
+ - lib/one_time_password/one_time_authentication_model.rb
99
+ - lib/one_time_password/railtie.rb
100
+ - lib/one_time_password/version.rb
101
+ - lib/tasks/one_time_password_tasks.rake
102
+ homepage: https://github.com/yosipy/one_time_password
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ homepage_uri: https://github.com/yosipy/one_time_password
107
+ source_code_uri: https://github.com/yosipy/one_time_password
108
+ changelog_uri: https://github.com/yosipy/one_time_password/blob/main/CHANGELOG.md
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.2.32
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: This Gem can be used to create 2FA (Two-Factor Authentication) function,
128
+ email address verification function for member registration and etc in Ruby on Rails.
129
+ test_files: []