mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 22:05:07 +00:00
167 lines
5.2 KiB
Ruby
167 lines
5.2 KiB
Ruby
class StepUpController < ApplicationController
|
|
helper_method :step_up_cancel_path
|
|
|
|
def new
|
|
@action = params[:action_type] # e.g., "remove_totp", "disable_2fa", "oidc_reauth", "email_change"
|
|
@return_to = params[:return_to]
|
|
@available_methods = current_identity.available_step_up_methods
|
|
@available_methods << :email unless @action == "email_change" # Email fallback not available for email change (already verifying old email)
|
|
@code_sent = params[:code_sent].present?
|
|
end
|
|
|
|
def send_email_code
|
|
if params[:action_type] == "email_change"
|
|
flash[:error] = "Email verification is not available for this action"
|
|
redirect_to new_step_up_path(action_type: params[:action_type], return_to: params[:return_to])
|
|
return
|
|
end
|
|
|
|
send_step_up_email_code
|
|
flash[:notice] = "A verification code has been sent to your email"
|
|
redirect_to new_step_up_path(
|
|
action_type: params[:action_type],
|
|
method: :email,
|
|
return_to: params[:return_to],
|
|
code_sent: true
|
|
)
|
|
end
|
|
|
|
def verify
|
|
action_type = params[:action_type]
|
|
method = params[:method]&.to_sym
|
|
code = params[:code]
|
|
|
|
if code.blank?
|
|
flash[:error] = "Please enter your verification code"
|
|
redirect_to new_step_up_path(action_type: action_type, method: method, return_to: params[:return_to], code_sent: method == :email)
|
|
return
|
|
end
|
|
|
|
if action_type == "email_change" && method == :email
|
|
flash[:error] = "Email verification is not available for this action"
|
|
redirect_to new_step_up_path(action_type: action_type, return_to: params[:return_to])
|
|
return
|
|
end
|
|
|
|
# Verify based on the method they chose
|
|
verified = case method
|
|
when :totp
|
|
totp = current_identity.totp
|
|
totp&.verify(code, drift_behind: 1, drift_ahead: 1)
|
|
|
|
when :backup_code
|
|
backup = current_identity.backup_codes.active.find { |bc| bc.authenticate_code(code) }
|
|
if backup
|
|
backup.mark_used!
|
|
true
|
|
else
|
|
false
|
|
end
|
|
|
|
when :email
|
|
login_code = current_identity.v2_login_codes.active.find_by(code: code.delete("-").strip)
|
|
if login_code
|
|
login_code.update!(used_at: Time.current)
|
|
true
|
|
else
|
|
false
|
|
end
|
|
else
|
|
false
|
|
end
|
|
|
|
unless verified
|
|
flash[:error] = "Invalid verification code"
|
|
redirect_to new_step_up_path(action_type: action_type, method: method, return_to: params[:return_to], code_sent: method == :email)
|
|
return
|
|
end
|
|
|
|
# Mark step-up as completed on the identity session, bound to the specific action
|
|
current_session.record_step_up!(action: action_type)
|
|
|
|
# Execute the verified action
|
|
case action_type
|
|
when "remove_totp"
|
|
totp = current_identity.totp
|
|
totp&.destroy
|
|
TwoFactorMailer.authentication_method_disabled(current_identity).deliver_later
|
|
|
|
if current_identity.two_factor_methods.empty?
|
|
current_identity.update!(use_two_factor_authentication: false)
|
|
current_identity.backup_codes.active.each(&:mark_discarded!)
|
|
end
|
|
|
|
consume_step_up!
|
|
redirect_to security_path, notice: "Two-factor authentication disabled"
|
|
|
|
when "disable_2fa"
|
|
current_identity.update!(use_two_factor_authentication: false)
|
|
TwoFactorMailer.required_authentication_disabled(current_identity).deliver_later
|
|
consume_step_up!
|
|
redirect_to security_path, notice: "2FA requirement disabled"
|
|
|
|
when "oidc_reauth"
|
|
# OIDC re-authentication completed, redirect back to OAuth flow
|
|
safe_path = safe_internal_redirect(params[:return_to])
|
|
redirect_to safe_path || root_path
|
|
|
|
when "email_change"
|
|
# Email change step-up completed, redirect to the email change form
|
|
safe_path = safe_internal_redirect(params[:return_to])
|
|
redirect_to safe_path || new_email_change_path
|
|
|
|
else
|
|
redirect_to security_path, alert: "Unknown action"
|
|
end
|
|
end
|
|
|
|
def resend_email
|
|
if params[:action_type] == "email_change"
|
|
flash[:error] = "Email verification is not available for this action"
|
|
redirect_to new_step_up_path(action_type: params[:action_type], return_to: params[:return_to])
|
|
return
|
|
end
|
|
|
|
send_step_up_email_code
|
|
flash[:notice] = "A new code has been sent to your email"
|
|
redirect_to new_step_up_path(
|
|
action_type: params[:action_type],
|
|
method: :email,
|
|
return_to: params[:return_to],
|
|
code_sent: true
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def send_step_up_email_code
|
|
login_code = current_identity.v2_login_codes.create!
|
|
IdentityMailer.v2_login_code(login_code).deliver_later
|
|
end
|
|
|
|
def step_up_cancel_path(action_type)
|
|
case action_type
|
|
when "email_change"
|
|
edit_identity_path
|
|
else
|
|
security_path
|
|
end
|
|
end
|
|
|
|
# Prevent open redirect attacks - only allow internal paths
|
|
def safe_internal_redirect(return_to)
|
|
return nil if return_to.blank?
|
|
|
|
uri = URI.parse(return_to) rescue nil
|
|
return nil unless uri
|
|
|
|
# Reject if it has a scheme or host (absolute URL or protocol-relative like //evil.com)
|
|
return nil if uri.scheme || uri.host
|
|
|
|
# Must be a path starting with /
|
|
return nil unless uri.path&.start_with?("/")
|
|
|
|
# Return just the path + query string
|
|
[ uri.path, uri.query ].compact.join("?")
|
|
end
|
|
end
|