Skip to content

Commit de55046

Browse files
feat: add TwoFactorController with OmniAuth-style routes and URL helpers
Add TwoFactorController following the OmniAuth callbacks pattern: a single controller with per-method new_<method> actions, a central POST create endpoint, and an ActiveSupport.on_load hook for extensions. Generate per-method challenge routes from mapping.to.two_factor_methods. Add generic URL helpers (new_two_factor_challenge_path, two_factor_path) included via engine initializer when 2FA methods are registered.
1 parent 5817f4d commit de55046

5 files changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
class Devise::TwoFactorController < DeviseController
4+
prepend_before_action :require_no_authentication
5+
prepend_before_action :ensure_sign_in_initiated
6+
7+
# Extensions can inject custom actions or override defaults via on_load
8+
ActiveSupport.run_load_hooks(:devise_two_factor_controller, self)
9+
10+
# Auto-generate default new_<module> actions for each registered 2FA module.
11+
# Extensions that injected a custom action via on_load won't be overwritten.
12+
Devise.two_factor_method_configs.each_key do |mod|
13+
unless method_defined?(:"new_#{mod}")
14+
define_method(:"new_#{mod}") do
15+
@resource = find_pending_resource
16+
end
17+
end
18+
end
19+
20+
# POST /users/two_factor
21+
# All methods POST here. Warden picks the right strategy via valid?.
22+
def create
23+
self.resource = warden.authenticate!(auth_options)
24+
set_flash_message!(:notice, :signed_in, scope: :"devise.sessions")
25+
sign_in(resource_name, resource)
26+
yield resource if block_given?
27+
respond_with resource, location: after_sign_in_path_for(resource)
28+
end
29+
30+
protected
31+
32+
def auth_options
33+
resource = find_pending_resource
34+
default_method = resource.enabled_two_factors.first
35+
{ scope: resource_name, recall: "#{controller_path}#new_#{default_method}" }
36+
end
37+
38+
def translation_scope
39+
'devise.two_factor'
40+
end
41+
42+
def find_pending_resource
43+
return unless session[:devise_two_factor_resource_id]
44+
resource_class.where(id: session[:devise_two_factor_resource_id]).first
45+
end
46+
47+
private
48+
49+
def ensure_sign_in_initiated
50+
return if session[:devise_two_factor_resource_id].present?
51+
set_flash_message!(:alert, :sign_in_not_initiated, scope: :"devise.failure")
52+
redirect_to new_session_path(resource_name)
53+
end
54+
end

lib/devise/rails.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class Engine < ::Rails::Engine
3737
end
3838
end
3939

40+
initializer "devise.two_factor" do
41+
config.after_initialize do
42+
if Devise.two_factor_method_configs.any?
43+
Devise.include_helpers(Devise::TwoFactor)
44+
end
45+
end
46+
end
47+
4048
initializer "devise.secret_key" do |app|
4149
Devise.secret_key ||= app.secret_key_base
4250

lib/devise/rails/routes.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,25 @@ def devise_unlock(mapping, controllers) #:nodoc:
398398
end
399399
end
400400

401+
def devise_two_factor(mapping, controllers) #:nodoc:
402+
return unless mapping.to.respond_to?(:two_factor_methods) && mapping.to.two_factor_methods.present?
403+
404+
controller = controllers[:two_factor] || "devise/two_factor"
405+
two_factor_path = mapping.path_names[:two_factor] || "two_factor"
406+
407+
# Central POST endpoint — all methods submit here
408+
post two_factor_path,
409+
to: "#{controller}#create",
410+
as: "two_factor"
411+
412+
# Per-method challenge routes
413+
Array(mapping.to.two_factor_methods).each do |method_name|
414+
get "#{two_factor_path}/#{method_name}/new",
415+
to: "#{controller}#new_#{method_name}",
416+
as: "new_two_factor_#{method_name}"
417+
end
418+
end
419+
401420
def devise_registration(mapping, controllers) #:nodoc:
402421
path_names = {
403422
new: mapping.path_names[:sign_up],

lib/devise/two_factor.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Devise
4+
module TwoFactor
5+
autoload :UrlHelpers, "devise/two_factor/url_helpers"
6+
end
7+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module Devise
4+
module TwoFactor
5+
module UrlHelpers
6+
def new_two_factor_challenge_path(resource_or_scope, method, *args)
7+
scope = Devise::Mapping.find_scope!(resource_or_scope)
8+
_devise_route_context.send(:"#{scope}_new_two_factor_#{method}_path", *args)
9+
end
10+
11+
def new_two_factor_challenge_url(resource_or_scope, method, *args)
12+
scope = Devise::Mapping.find_scope!(resource_or_scope)
13+
_devise_route_context.send(:"#{scope}_new_two_factor_#{method}_url", *args)
14+
end
15+
16+
def two_factor_path(resource_or_scope, *args)
17+
scope = Devise::Mapping.find_scope!(resource_or_scope)
18+
_devise_route_context.send(:"#{scope}_two_factor_path", *args)
19+
end
20+
21+
def two_factor_url(resource_or_scope, *args)
22+
scope = Devise::Mapping.find_scope!(resource_or_scope)
23+
_devise_route_context.send(:"#{scope}_two_factor_url", *args)
24+
end
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)