summaryrefslogtreecommitdiff
path: root/lib/authenticated_system.rb
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-01 01:10:35 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-01 01:10:35 +0200
commitf6c1f0f08f031778a491465d35ac694bfcdc12b0 (patch)
tree99aec0f419b1122821124fdb7ae8bdf4bc5831db /lib/authenticated_system.rb
parentdf90138fb55f7d3652d3d69d58325d7329920f51 (diff)
Require a fresh second factor for user management
Administrative actions are gated behind a 30-minute elevation window: creating and retiring accounts, editing roles, clearing a second factor. Reading the list is not gated, and content work is untouched. elevated? is tied to is_admin?, so losing the role closes the window at once. The window opens when the second factor verifies at login, so an admin heading straight for user management is already elevated, and closes on logout with the other session state. Five wrong codes end the session, mirroring the login challenge. users#update carries no elevation filter, since self-service reaches it; the role field is gated in user_params instead and fails closed.
Diffstat (limited to 'lib/authenticated_system.rb')
-rw-r--r--lib/authenticated_system.rb27
1 files changed, 25 insertions, 2 deletions
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb
index 4e70c28d..9a351dde 100644
--- a/lib/authenticated_system.rb
+++ b/lib/authenticated_system.rb
@@ -1,5 +1,6 @@
1module AuthenticatedSystem 1module AuthenticatedSystem
2 SESSION_MAX_AGE = 7.days 2 SESSION_MAX_AGE = 7.days
3 ELEVATION_MAX_AGE = 30.minutes
3 4
4 protected 5 protected
5 # Returns true or false if the user is logged in. 6 # Returns true or false if the user is logged in.
@@ -20,6 +21,26 @@ module AuthenticatedSystem
20 @current_user = new_user || false 21 @current_user = new_user || false
21 end 22 end
22 23
24 # Tied to is_admin? so losing the role closes the window at once, rather
25 # than leaving a timestamp that would count again if the role returned.
26 def elevated?
27 return false unless current_user&.is_admin?
28 session[:elevated_at].to_i > ELEVATION_MAX_AGE.ago.to_i
29 end
30
31 def elevation_expires_at
32 return nil unless elevated?
33 Time.at(session[:elevated_at].to_i) + ELEVATION_MAX_AGE
34 end
35
36 def elevate!
37 session[:elevated_at] = Time.now.to_i
38 end
39
40 def drop_elevation!
41 session.delete(:elevated_at)
42 end
43
23 # Check if the user is authorized 44 # Check if the user is authorized
24 # 45 #
25 # Override this method in your controllers if you want to restrict access 46 # Override this method in your controllers if you want to restrict access
@@ -91,7 +112,8 @@ module AuthenticatedSystem
91 # Inclusion hook to make #current_user and #logged_in? 112 # Inclusion hook to make #current_user and #logged_in?
92 # available as ActionView helper methods. 113 # available as ActionView helper methods.
93 def self.included(base) 114 def self.included(base)
94 base.send :helper_method, :current_user, :logged_in?, :authorized? if base.respond_to? :helper_method 115 base.send :helper_method, :current_user, :logged_in?, :authorized?,
116 :elevated?, :elevation_expires_at if base.respond_to? :helper_method
95 end 117 end
96 118
97 # 119 #
@@ -123,7 +145,8 @@ module AuthenticatedSystem
123 def logout_keeping_session! 145 def logout_keeping_session!
124 @current_user = false # not logged in, and don't do it for me 146 @current_user = false # not logged in, and don't do it for me
125 session[:user_id] = nil # keeps the session but kill our variable 147 session[:user_id] = nil # keeps the session but kill our variable
126 # explicitly kill any other session variables you set 148 session.delete(:elevated_at)
149 session.delete(:elevation_attempts)
127 end 150 end
128 151
129 # The session should only be reset at the tail end of a form POST -- 152 # The session should only be reset at the tail end of a form POST --