rails_mfa 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4022b6c7e142e946603768e2ff0066f3a2698c3b20a663fe29305ac670b8fcfc
4
- data.tar.gz: 077046b6cbb8a8261bfc4f9942df3779b6a1dd9d062853d61abc43224abb8c35
3
+ metadata.gz: 0763347276b010decd3b0ffcd1bf959b8752dcbdabbbebe09c24298bcd3816f7
4
+ data.tar.gz: 3d2f11cfd6fd166c9ff1ee0f3ed191041548563c1fc121b665132736e01203e8
5
5
  SHA512:
6
- metadata.gz: 9d9dd6854a9209dc5cb45390148f7f965d8106b29ad34203c528f277bb84ccb2c5a8d4393811b0579092cf7bdae52803671119c8c90415f5449ac666e401fb22
7
- data.tar.gz: 751deab61328d9e39d12bccbbd057ab719db5afca3bc8973993bd225574b9a9abd686dd1abed19b650f3fe8e80365992c84909c0831f888922229783cab837a5
6
+ metadata.gz: 50f1097add2c856ca93ac36c59bf02177b103ac3312e277373b4c485ef8f34865a54400f0e117af00ad45bb408cac7708add5d1605ff0990a9d71334ee42886a
7
+ data.tar.gz: 8eb7044c13c90741b4a99447adeea254d549586cb78a74cd4c02880bfbb2db887838de4e422c038701b3c7cc86b8b0c6b046b9a7230f486910afe8915f44899f
data/.rubocop.yml CHANGED
@@ -1,5 +1,52 @@
1
1
  AllCops:
2
+ NewCops: enable
2
3
  TargetRubyVersion: 3.1
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - 'bin/**/*'
7
+ - 'vendor/**/*'
8
+ - 'tmp/**/*'
9
+
10
+ # Spec files naturally have longer blocks for describing test scenarios
11
+ # Gemspec naturally has a long block
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - 'spec/**/*_spec.rb'
15
+ - 'spec/spec_helper.rb'
16
+ - '*.gemspec'
17
+
18
+ # Gemspec can have longer lines for descriptions
19
+ Layout/LineLength:
20
+ Max: 120
21
+ Exclude:
22
+ - '*.gemspec'
23
+ - 'Gemfile'
24
+
25
+ # Allow longer method names in specs and model methods that handle multiple cases
26
+ Metrics/MethodLength:
27
+ Exclude:
28
+ - 'spec/**/*'
29
+ - 'lib/rails_mfa/model.rb'
30
+
31
+ # Allow more complex code in specs and model methods
32
+ Metrics/AbcSize:
33
+ Exclude:
34
+ - 'spec/**/*'
35
+ - 'lib/rails_mfa/model.rb'
36
+
37
+ # Documentation can be added later - focus on functionality first
38
+ Style/Documentation:
39
+ Enabled: false
40
+
41
+ # Empty blocks in specs are intentional for testing edge cases
42
+ Lint/EmptyBlock:
43
+ Exclude:
44
+ - 'spec/**/*'
45
+
46
+ # Provider initialize methods don't need super
47
+ Lint/MissingSuper:
48
+ Exclude:
49
+ - 'lib/rails_mfa/providers/*.rb'
3
50
 
4
51
  Style/StringLiterals:
5
52
  EnforcedStyle: double_quotes
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2025-11-10
11
+
12
+ ### Fixed
13
+ - RuboCop linting offenses resolved
14
+ - Added RubyGems MFA requirement metadata for enhanced security
15
+ - Fixed unused block arguments in test files
16
+ - Improved code formatting and readability
17
+
18
+ ## [0.1.0] - 2025-11-06
19
+
10
20
  ### Added
11
21
  - Rails generators for easy installation and setup
12
22
  - `rails generate rails_mfa:install` - Creates initializer with configuration examples
@@ -18,8 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
28
  - Dedicated controller examples for authenticator app setup flow
19
29
  - Improved README emphasizing provider-agnostic nature
20
30
 
21
- ## [0.1.0] - 2025-11-06
22
-
23
31
  ### Added
24
32
  - Initial release of RailsMFA gem
25
33
  - Support for SMS-based multi-factor authentication
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RailsMfa
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a RailsMFA initializer in your application."
11
+
12
+ def copy_initializer
13
+ template "rails_mfa.rb", "config/initializers/rails_mfa.rb"
14
+ end
15
+
16
+ def show_readme
17
+ readme "README" if behavior == :invoke
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/active_record"
5
+
6
+ module RailsMfa
7
+ module Generators
8
+ class MigrationGenerator < Rails::Generators::NamedBase
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Generates a migration to add MFA columns to a model"
14
+
15
+ argument :name, type: :string, default: "User",
16
+ desc: "The name of the model to add MFA columns to (e.g., User, Account)"
17
+
18
+ def self.next_migration_number(dirname)
19
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
20
+ end
21
+
22
+ def create_migration_file
23
+ migration_template(
24
+ "migration.rb.erb",
25
+ "db/migrate/add_mfa_to_#{table_name}.rb"
26
+ )
27
+ end
28
+
29
+ def show_instructions
30
+ return unless behavior == :invoke
31
+
32
+ say ""
33
+ say "Migration created!", :green
34
+ say ""
35
+ say "Next steps:", :yellow
36
+ say " 1. Review the migration file"
37
+ say " 2. Run: rails db:migrate"
38
+ say " 3. Include RailsMFA::Model in your #{class_name} model"
39
+ say ""
40
+ end
41
+
42
+ private
43
+
44
+ def table_name
45
+ name.tableize
46
+ end
47
+
48
+ def class_name
49
+ name.classify
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ ===============================================================================
2
+
3
+ RailsMFA has been installed!
4
+
5
+ Next steps:
6
+
7
+ 1. Add MFA columns to your User model:
8
+
9
+ rails generate rails_mfa:migration User
10
+
11
+ 2. Run the migration:
12
+
13
+ rails db:migrate
14
+
15
+ 3. Configure your SMS and Email providers in:
16
+
17
+ config/initializers/rails_mfa.rb
18
+
19
+ 4. Include RailsMFA::Model in your User model:
20
+
21
+ # app/models/user.rb
22
+ class User < ApplicationRecord
23
+ include RailsMFA::Model
24
+
25
+ # Optionally specify which MFA methods are supported
26
+ enable_mfa_for :sms, :email, :totp
27
+ end
28
+
29
+ 5. Add routes and controllers for MFA verification (see documentation)
30
+
31
+ For detailed usage instructions, visit:
32
+ https://github.com/shoaibmalik786/rails_mfa
33
+
34
+ ===============================================================================
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddMfaTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ add_column :<%= table_name %>, :mfa_secret, :string
6
+ add_column :<%= table_name %>, :mfa_enabled, :boolean, default: false, null: false
7
+ add_column :<%= table_name %>, :phone, :string
8
+ add_column :<%= table_name %>, :mfa_method, :string
9
+
10
+ add_index :<%= table_name %>, :mfa_enabled
11
+ end
12
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsMFA.configure do |config|
4
+ # ============================================================================
5
+ # SMS Provider Configuration
6
+ # ============================================================================
7
+ # Define your SMS provider here. This is a lambda that receives (phone_number, message)
8
+ # and sends the SMS. You can use any SMS service (Twilio, AWS SNS, Vonage, etc.)
9
+ #
10
+ # Example with Twilio:
11
+ # config.sms_provider = lambda do |to, message|
12
+ # twilio_client = Twilio::REST::Client.new(
13
+ # ENV['TWILIO_ACCOUNT_SID'],
14
+ # ENV['TWILIO_AUTH_TOKEN']
15
+ # )
16
+ # twilio_client.messages.create(
17
+ # from: ENV['TWILIO_PHONE_NUMBER'],
18
+ # to: to,
19
+ # body: message
20
+ # )
21
+ # end
22
+ #
23
+ # Example with AWS SNS:
24
+ # config.sms_provider = lambda do |to, message|
25
+ # sns = Aws::SNS::Client.new(region: ENV['AWS_REGION'])
26
+ # sns.publish(phone_number: to, message: message)
27
+ # end
28
+ #
29
+ config.sms_provider = lambda do |to, message|
30
+ # TODO: Implement your SMS provider here
31
+ Rails.logger.info "SMS to #{to}: #{message}"
32
+ end
33
+
34
+ # ============================================================================
35
+ # Email Provider Configuration
36
+ # ============================================================================
37
+ # Define your email provider here. This is a lambda that receives (email, subject, body)
38
+ # and sends the email. You can use ActionMailer or any email service.
39
+ #
40
+ # Example with ActionMailer:
41
+ # config.email_provider = lambda do |to, subject, body|
42
+ # MfaMailer.send_code(to, subject, body).deliver_now
43
+ # end
44
+ #
45
+ # Example with SendGrid:
46
+ # config.email_provider = lambda do |to, subject, body|
47
+ # mail = SendGrid::Mail.new(
48
+ # from: SendGrid::Email.new(email: 'noreply@example.com'),
49
+ # subject: subject,
50
+ # to: SendGrid::Email.new(email: to),
51
+ # content: SendGrid::Content.new(type: 'text/plain', value: body)
52
+ # )
53
+ # sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
54
+ # sg.client.mail._('send').post(request_body: mail.to_json)
55
+ # end
56
+ #
57
+ config.email_provider = lambda do |to, subject, body|
58
+ # TODO: Implement your email provider here
59
+ Rails.logger.info "Email to #{to}: #{subject} - #{body}"
60
+ end
61
+
62
+ # ============================================================================
63
+ # Token Configuration
64
+ # ============================================================================
65
+ # Length of the numeric verification code (default: 6)
66
+ config.code_length = 6
67
+
68
+ # How long the verification code is valid in seconds (default: 300 = 5 minutes)
69
+ config.code_expiry_seconds = 300
70
+
71
+ # ============================================================================
72
+ # Cache Store Configuration
73
+ # ============================================================================
74
+ # Token storage backend (default: Rails.cache)
75
+ # You can use Redis for better performance:
76
+ # config.token_store = Redis.new(host: ENV['REDIS_HOST'], port: ENV['REDIS_PORT'], db: 1)
77
+ #
78
+ # Or use the default Rails cache:
79
+ # config.token_store = Rails.cache
80
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMFA
4
+ class Configuration
5
+ attr_accessor :sms_provider, :email_provider, :token_store, :code_expiry_seconds, :code_length
6
+
7
+ def initialize
8
+ # These lambdas are defined by the host app
9
+ @sms_provider = nil # -> lambda: ->(to, message) { ... }
10
+ @email_provider = nil # -> lambda: ->(to, subject, body) { ... }
11
+
12
+ # Use Rails.cache by default if Rails is loaded
13
+ @token_store = defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : SimpleStore.new
14
+
15
+ @code_expiry_seconds = 300 # 5 minutes
16
+ @code_length = 6
17
+ end
18
+ end
19
+
20
+ # Fallback in case Rails.cache is unavailable (for plain Ruby apps)
21
+ class SimpleStore
22
+ def initialize
23
+ @store = {}
24
+ end
25
+
26
+ def write(key, value, expires_in: nil)
27
+ @store[key] = { value: value, expires_at: expires_in ? Time.now + expires_in : nil }
28
+ end
29
+
30
+ def read(key)
31
+ entry = @store[key]
32
+ return nil unless entry
33
+ return nil if entry[:expires_at] && Time.now > entry[:expires_at]
34
+
35
+ entry[:value]
36
+ end
37
+
38
+ def delete(key)
39
+ @store.delete(key)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "rotp"
5
+ require "rqrcode"
6
+
7
+ module RailsMFA
8
+ module Model
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ def enable_mfa_for(*methods)
13
+ class_attribute :rails_mfa_methods, instance_accessor: false
14
+ self.rails_mfa_methods = methods.map(&:to_sym)
15
+ end
16
+ end
17
+
18
+ def generate_totp_secret!
19
+ secret = ROTP::Base32.random_base32
20
+ # host app should store secret encrypted in a column like :mfa_secret
21
+ update!(mfa_secret: secret) if respond_to?(:update!)
22
+ secret
23
+ end
24
+
25
+ def totp_provisioning_uri(issuer: "RailsMFA")
26
+ raise "No mfa_secret present" unless respond_to?(:mfa_secret) && mfa_secret
27
+
28
+ ROTP::TOTP.new(mfa_secret, issuer: issuer).provisioning_uri(respond_to?(:email) ? email : "user")
29
+ end
30
+
31
+ def verify_totp(code)
32
+ return false unless respond_to?(:mfa_secret) && mfa_secret
33
+
34
+ totp = ROTP::TOTP.new(mfa_secret)
35
+ totp.verify(code, drift_behind: 30)
36
+ end
37
+
38
+ def send_numeric_code(via: :sms)
39
+ tm = TokenManager.new
40
+ code = tm.generate_numeric_code(id)
41
+ case via.to_sym
42
+ when :sms
43
+ raise "sms_provider not configured" unless RailsMFA.configuration.sms_provider
44
+
45
+ RailsMFA.configuration.sms_provider.call(phone_number_for_sms, "Your verification code is: #{code}")
46
+ when :email
47
+ raise "email_provider not configured" unless RailsMFA.configuration.email_provider
48
+
49
+ RailsMFA.configuration.email_provider.call(email, "Your verification code", "Code: #{code}")
50
+ else
51
+ raise "Unsupported channel"
52
+ end
53
+ code
54
+ end
55
+
56
+ def verify_numeric_code(code)
57
+ tm = TokenManager.new
58
+ tm.verify_numeric_code(id, code)
59
+ end
60
+
61
+ private
62
+
63
+ def phone_number_for_sms
64
+ # host app should implement proper phone number attribute
65
+ respond_to?(:phone) ? phone : raise("Define phone attribute or override phone_number_for_sms")
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMFA
4
+ module Providers
5
+ class Base
6
+ def send_sms(to, message)
7
+ raise NotImplementedError, "Implement send_sms in provider"
8
+ end
9
+
10
+ def send_email(to, subject, body)
11
+ raise NotImplementedError, "Implement send_email in provider"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMFA
4
+ module Providers
5
+ class EmailProvider < Base
6
+ def initialize(&block)
7
+ @block = block
8
+ end
9
+
10
+ def send_email(to, subject, body)
11
+ @block.call(to, subject, body)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMFA
4
+ module Providers
5
+ # Example: host app can create a provider class that implements send_sms
6
+ class SmsProvider < Base
7
+ def initialize(&block)
8
+ @block = block
9
+ end
10
+
11
+ def send_sms(to, message)
12
+ @block.call(to, message)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "active_support/security_utils"
5
+
6
+ module RailsMFA
7
+ class TokenManager
8
+ def initialize(store: RailsMFA.configuration.token_store)
9
+ @store = store
10
+ end
11
+
12
+ def generate_numeric_code(user_id, length: RailsMFA.configuration.code_length,
13
+ expiry: RailsMFA.configuration.code_expiry_seconds)
14
+ min = 10**(length - 1)
15
+ max = (10**length) - 1
16
+ code = rand(min..max).to_s
17
+ @store.write(cache_key(user_id), code, expires_in: expiry)
18
+ code
19
+ end
20
+
21
+ def verify_numeric_code(user_id, code)
22
+ stored = @store.read(cache_key(user_id))
23
+ return false unless stored
24
+
25
+ valid = ActiveSupport::SecurityUtils.secure_compare(stored.to_s, code.to_s)
26
+ @store.delete(cache_key(user_id)) if valid # one-time use
27
+ valid
28
+ end
29
+
30
+ private
31
+
32
+ def cache_key(user_id)
33
+ "rails_mfa:otp:#{user_id}"
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMFA
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_mfa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shoaib Malik
@@ -68,7 +68,18 @@ files:
68
68
  - LICENSE.txt
69
69
  - README.md
70
70
  - Rakefile
71
+ - lib/generators/rails_mfa/install_generator.rb
72
+ - lib/generators/rails_mfa/migration_generator.rb
73
+ - lib/generators/rails_mfa/templates/README
74
+ - lib/generators/rails_mfa/templates/migration.rb.erb
75
+ - lib/generators/rails_mfa/templates/rails_mfa.rb
71
76
  - lib/rails_mfa.rb
77
+ - lib/rails_mfa/configuration.rb
78
+ - lib/rails_mfa/model.rb
79
+ - lib/rails_mfa/providers/base.rb
80
+ - lib/rails_mfa/providers/email_provider.rb
81
+ - lib/rails_mfa/providers/sms_provider.rb
82
+ - lib/rails_mfa/token_manager.rb
72
83
  - lib/rails_mfa/version.rb
73
84
  - sig/rails_mfa.rbs
74
85
  homepage: https://github.com/shoaibmalik786/rails_mfa