diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index ebc5fa8..202e93b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,6 +1,7 @@ class SessionsController < ApplicationController - allow_unauthenticated_access only: %i[ new create ] + allow_unauthenticated_access only: %i[ new create verify_totp ] rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." } + rate_limit to: 5, within: 3.minutes, only: :verify_totp, with: -> { redirect_to totp_verification_path, alert: "Too many attempts. Try again later." } def new # Redirect to signup if this is first run @@ -23,9 +24,9 @@ class SessionsController < ApplicationController # Check if TOTP is required if user.totp_enabled? - # TODO: Implement TOTP verification flow - # For now, reject login if TOTP is enabled - redirect_to signin_path, alert: "Two-factor authentication is enabled but not yet implemented. Please contact an administrator." + # Store user ID in session temporarily for TOTP verification + session[:pending_totp_user_id] = user.id + redirect_to totp_verification_path return end @@ -34,6 +35,49 @@ class SessionsController < ApplicationController redirect_to after_authentication_url, notice: "Signed in successfully." end + def verify_totp + # Get the pending user from session + user_id = session[:pending_totp_user_id] + unless user_id + redirect_to signin_path, alert: "Session expired. Please sign in again." + return + end + + user = User.find_by(id: user_id) + unless user + session.delete(:pending_totp_user_id) + redirect_to signin_path, alert: "Session expired. Please sign in again." + return + end + + # Handle form submission + if request.post? + code = params[:code]&.strip + + # Try TOTP verification first + if user.verify_totp(code) + session.delete(:pending_totp_user_id) + start_new_session_for user + redirect_to after_authentication_url, notice: "Signed in successfully." + return + end + + # Try backup code verification + if user.verify_backup_code(code) + session.delete(:pending_totp_user_id) + start_new_session_for user + redirect_to after_authentication_url, notice: "Signed in successfully using backup code." + return + end + + # Invalid code + redirect_to totp_verification_path, alert: "Invalid verification code. Please try again." + return + end + + # Just render the form + end + def destroy terminate_session redirect_to signin_path, status: :see_other, notice: "Signed out successfully." diff --git a/app/views/sessions/verify_totp.html.erb b/app/views/sessions/verify_totp.html.erb new file mode 100644 index 0000000..012540b --- /dev/null +++ b/app/views/sessions/verify_totp.html.erb @@ -0,0 +1,56 @@ +
+
+
+

Two-Factor Authentication

+

+ Enter the 6-digit code from your authenticator app to complete sign in. +

+
+ + <%= form_with url: totp_verification_path, method: :post, class: "space-y-6" do |form| %> +
+ <%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %> + <%= text_field_tag :code, + nil, + placeholder: "000000", + maxlength: 8, + required: true, + autofocus: true, + autocomplete: "off", + class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-center text-2xl tracking-widest font-mono sm:text-sm" %> +

+ Enter your 6-digit authenticator code or an 8-character backup code +

+
+ +
+ <%= form.submit "Verify", + class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> +
+ <% end %> + +
+
+
+
+
+
+ Need help? +
+
+ +
+

+ Lost access to your authenticator? +

+

+ Contact an administrator for assistance. +

+
+ +
+ <%= link_to "Cancel and sign in again", signin_path, class: "text-sm text-blue-600 hover:text-blue-500" %> +
+
+
+
diff --git a/config/routes.rb b/config/routes.rb index 5127eab..7ae9d2b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,8 @@ Rails.application.routes.draw do get "/signin", to: "sessions#new", as: :signin post "/signin", to: "sessions#create" delete "/signout", to: "sessions#destroy", as: :signout + get "/totp-verification", to: "sessions#verify_totp", as: :totp_verification + post "/totp-verification", to: "sessions#verify_totp" # Authenticated routes root "dashboard#index"