149 lines
5.3 KiB
Ruby
149 lines
5.3 KiB
Ruby
class TotpController < ApplicationController
|
|
before_action :set_user
|
|
before_action :redirect_if_totp_enabled, only: [:new, :create]
|
|
before_action :require_totp_enabled, only: [:backup_codes, :verify_password, :destroy]
|
|
|
|
# GET /totp/new - Show QR code to set up TOTP
|
|
def new
|
|
# Check if user is being forced to set up TOTP by admin
|
|
@totp_setup_required = session[:pending_totp_setup_user_id].present?
|
|
|
|
# Generate TOTP secret but don't save yet
|
|
@totp_secret = ROTP::Base32.random
|
|
@provisioning_uri = ROTP::TOTP.new(@totp_secret, issuer: "Clinch").provisioning_uri(@user.email_address)
|
|
|
|
# Generate QR code
|
|
require "rqrcode"
|
|
@qr_code = RQRCode::QRCode.new(@provisioning_uri)
|
|
end
|
|
|
|
# POST /totp - Verify TOTP code and enable 2FA
|
|
def create
|
|
totp_secret = params[:totp_secret]
|
|
code = params[:code]
|
|
|
|
# Verify the code works
|
|
totp = ROTP::TOTP.new(totp_secret)
|
|
if totp.verify(code, drift_behind: 30, drift_ahead: 30)
|
|
# Save the secret and generate backup codes
|
|
@user.totp_secret = totp_secret
|
|
plain_codes = @user.send(:generate_backup_codes) # Use private method from User model
|
|
@user.save!
|
|
|
|
# Store plain codes temporarily in session for display after redirect
|
|
session[:temp_backup_codes] = plain_codes
|
|
|
|
# Check if this was a required setup from login
|
|
if session[:pending_totp_setup_user_id].present?
|
|
session.delete(:pending_totp_setup_user_id)
|
|
# Mark that user should be auto-signed in after viewing backup codes
|
|
session[:auto_signin_after_forced_totp] = true
|
|
redirect_to backup_codes_totp_path, notice: "Two-factor authentication has been enabled successfully! Save these backup codes, then you'll be signed in."
|
|
else
|
|
# Regular setup from profile
|
|
redirect_to backup_codes_totp_path, notice: "Two-factor authentication has been enabled successfully! Save these backup codes now."
|
|
end
|
|
else
|
|
redirect_to new_totp_path, alert: "Invalid verification code. Please try again."
|
|
end
|
|
end
|
|
|
|
# GET /totp/backup_codes - Show backup codes (requires password)
|
|
def backup_codes
|
|
# Check if we have temporary codes from TOTP setup
|
|
if session[:temp_backup_codes].present?
|
|
@backup_codes = session[:temp_backup_codes]
|
|
session.delete(:temp_backup_codes) # Clear after use
|
|
|
|
# Check if this was a forced TOTP setup during login
|
|
@auto_signin_pending = session[:auto_signin_after_forced_totp].present?
|
|
if @auto_signin_pending
|
|
session.delete(:auto_signin_after_forced_totp)
|
|
end
|
|
else
|
|
# This will be shown after password verification for existing users
|
|
# Since we can't display BCrypt hashes, redirect to regenerate
|
|
redirect_to regenerate_backup_codes_totp_path
|
|
end
|
|
end
|
|
|
|
# POST /totp/verify_password - Verify password before showing backup codes
|
|
def verify_password
|
|
if @user.authenticate(params[:password])
|
|
redirect_to backup_codes_totp_path
|
|
else
|
|
redirect_to profile_path, alert: "Incorrect password."
|
|
end
|
|
end
|
|
|
|
# GET /totp/regenerate_backup_codes - Regenerate backup codes (requires password)
|
|
def regenerate_backup_codes
|
|
# This will be shown after password verification
|
|
end
|
|
|
|
# POST /totp/regenerate_backup_codes - Actually regenerate backup codes
|
|
def create_new_backup_codes
|
|
unless @user.authenticate(params[:password])
|
|
redirect_to regenerate_backup_codes_totp_path, alert: "Incorrect password."
|
|
return
|
|
end
|
|
|
|
# Generate new backup codes and store BCrypt hashes
|
|
plain_codes = @user.send(:generate_backup_codes)
|
|
@user.save!
|
|
|
|
# Store plain codes temporarily in session for display
|
|
session[:temp_backup_codes] = plain_codes
|
|
|
|
redirect_to backup_codes_totp_path, notice: "New backup codes have been generated. Save them now!"
|
|
end
|
|
|
|
# POST /totp/complete_setup - Complete forced TOTP setup and sign in
|
|
def complete_setup
|
|
# Sign in the user after they've saved their backup codes
|
|
# This is only used when admin requires TOTP and user just set it up during login
|
|
if session[:totp_redirect_url].present?
|
|
session[:return_to_after_authenticating] = session.delete(:totp_redirect_url)
|
|
end
|
|
|
|
start_new_session_for @user
|
|
redirect_to after_authentication_url, notice: "Two-factor authentication enabled. Signed in successfully.", allow_other_host: true
|
|
end
|
|
|
|
# DELETE /totp - Disable TOTP (requires password)
|
|
def destroy
|
|
unless @user.authenticate(params[:password])
|
|
redirect_to profile_path, alert: "Incorrect password. Could not disable 2FA."
|
|
return
|
|
end
|
|
|
|
# Prevent disabling if admin requires TOTP
|
|
if @user.totp_required?
|
|
redirect_to profile_path, alert: "Two-factor authentication is required by your administrator and cannot be disabled."
|
|
return
|
|
end
|
|
|
|
@user.disable_totp!
|
|
redirect_to profile_path, notice: "Two-factor authentication has been disabled."
|
|
end
|
|
|
|
private
|
|
|
|
def set_user
|
|
@user = Current.session.user
|
|
end
|
|
|
|
def redirect_if_totp_enabled
|
|
# Allow setup if admin requires it, even if already enabled (for regeneration)
|
|
if @user.totp_enabled? && !session[:pending_totp_setup_user_id].present?
|
|
redirect_to profile_path, alert: "Two-factor authentication is already enabled."
|
|
end
|
|
end
|
|
|
|
def require_totp_enabled
|
|
unless @user.totp_enabled?
|
|
redirect_to profile_path, alert: "Two-factor authentication is not enabled."
|
|
end
|
|
end
|
|
end
|