Use the IPAddr library to detect ipv4 and ipv6 addresses
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-29 13:47:02 +11:00
parent c3205abffa
commit baa75a3456
6 changed files with 762 additions and 30 deletions

View File

@@ -50,23 +50,23 @@ module Api
if forwarded_host.present?
# Load active rules with their associations for better performance
# Preload groups to avoid N+1 queries in user_allowed? checks
rules = ForwardAuthRule.includes(:groups).active
rules = ForwardAuthRule.includes(:allowed_groups).active
# Find matching forward auth rule for this domain
rule = rules.find { |r| r.matches_domain?(forwarded_host) }
unless rule
Rails.logger.warn "ForwardAuth: No rule found for domain: #{forwarded_host}"
return render_forbidden("No authentication rule configured for this domain")
end
if rule
# Check if user is allowed by this rule
unless rule.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{forwarded_host} by rule #{rule.domain_pattern}"
return render_forbidden("You do not have permission to access this domain")
end
# Check if user is allowed by this rule
unless rule.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{forwarded_host} by rule #{rule.domain_pattern}"
return render_forbidden("You do not have permission to access this domain")
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{forwarded_host} by rule #{rule.domain_pattern} (policy: #{rule.policy_for_user(user)})"
else
# No rule found - allow access with default headers (original behavior)
Rails.logger.info "ForwardAuth: No rule found for domain: #{forwarded_host}, allowing with default headers"
end
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{forwarded_host} by rule #{rule.domain_pattern} (policy: #{rule.policy_for_user(user)})"
else
Rails.logger.info "ForwardAuth: User #{user.email_address} authenticated (no domain specified)"
end
@@ -138,7 +138,8 @@ module Api
response.headers["X-Auth-Reason"] = reason if reason
# Get the redirect URL from query params or construct default
base_url = params[:rd] || "https://clinch.aapamilne.com"
redirect_url = validate_redirect_url(params[:rd])
base_url = redirect_url || "https://clinch.aapamilne.com"
# Set the original URL that user was trying to access
# This will be used after authentication
@@ -149,11 +150,11 @@ module Api
Rails.logger.info "ForwardAuth Headers: Host=#{request.headers['Host']}, X-Forwarded-Host=#{original_host}, X-Forwarded-Uri=#{request.headers['X-Forwarded-Uri']}, X-Forwarded-Path=#{request.headers['X-Forwarded-Path']}"
original_url = if original_host
# Use the forwarded host and URI
# Use the forwarded host and URI (original behavior)
"https://#{original_host}#{original_uri}"
else
# Fallback: just redirect to the root of the original host
"https://#{request.headers['Host']}"
# Fallback: use the validated redirect URL or default
redirect_url || "https://clinch.aapamilne.com"
end
# Debug: log what we're redirecting to after login
@@ -183,5 +184,40 @@ module Api
# Return 403 Forbidden
head :forbidden
end
def validate_redirect_url(url)
return nil unless url.present?
begin
uri = URI.parse(url)
# Only allow HTTP/HTTPS schemes
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
# Only allow HTTPS in production
return nil unless Rails.env.development? || uri.scheme == 'https'
redirect_domain = uri.host.downcase
return nil unless redirect_domain.present?
# Check against our ForwardAuthRules
matching_rule = ForwardAuthRule.active.find do |rule|
rule.matches_domain?(redirect_domain)
end
matching_rule ? url : nil
rescue URI::InvalidURIError
nil
end
end
def domain_has_forward_auth_rule?(domain)
return false if domain.blank?
ForwardAuthRule.active.any? do |rule|
rule.matches_domain?(domain.downcase)
end
end
end
end

View File

@@ -1,5 +1,6 @@
require 'uri'
require 'public_suffix'
require 'ipaddr'
module Authentication
extend ActiveSupport::Concern
@@ -61,7 +62,7 @@ module Authentication
# Set domain for cross-subdomain authentication if we can extract it
cookie_options[:domain] = domain if domain.present?
cookies.signed.permanent[:session_id] = cookie_options
cookies.signed.permanent[:session_id] = cookie_options
# Create a one-time token for immediate forward auth after authentication
# This solves the race condition where browser hasn't processed cookie yet
@@ -80,7 +81,7 @@ module Authentication
# by setting cookies with the domain parameter (e.g., .example.com allows access from
# both app.example.com and api.example.com).
#
# CRITICAL: Returns nil for IP addresses and localhost - this is intentional!
# CRITICAL: Returns nil for IP addresses (IPv4 and IPv6) and localhost - this is intentional!
# When accessing services by IP, there are no subdomains to share cookies with,
# and setting a domain cookie would break authentication.
#
@@ -102,8 +103,8 @@ module Authentication
# Strip port number for domain parsing
host_without_port = host.split(':').first
# Check if it's an IP address - if so, don't set domain cookie
return nil if host_without_port.match?(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
# Check if it's an IP address (IPv4 or IPv6) - if so, don't set domain cookie
return nil if IPAddr.new(host_without_port) rescue false
# Use Public Suffix List for accurate domain parsing
domain = PublicSuffix.parse(host_without_port)
@@ -140,7 +141,6 @@ module Authentication
# Update the session with the tokenized URL
controller_session[:return_to_after_authenticating] = uri.to_s
end
end
end
end

View File

@@ -16,9 +16,10 @@ class SessionsController < ApplicationController
return
end
# Store the redirect URL from forward auth if present
# Store the redirect URL from forward auth if present (after validation)
if params[:rd].present?
session[:return_to_after_authenticating] = params[:rd]
validated_url = validate_redirect_url(params[:rd])
session[:return_to_after_authenticating] = validated_url if validated_url
end
# Check if user is active
@@ -35,9 +36,10 @@ class SessionsController < ApplicationController
if user.totp_enabled?
# Store user ID in session temporarily for TOTP verification
session[:pending_totp_user_id] = user.id
# Preserve the redirect URL through TOTP verification
# Preserve the redirect URL through TOTP verification (after validation)
if params[:rd].present?
session[:totp_redirect_url] = params[:rd]
validated_url = validate_redirect_url(params[:rd])
session[:totp_redirect_url] = validated_url if validated_url
end
redirect_to totp_verification_path(rd: params[:rd])
return
@@ -115,4 +117,33 @@ class SessionsController < ApplicationController
session.destroy
redirect_to profile_path, notice: "Session revoked successfully."
end
private
def validate_redirect_url(url)
return nil unless url.present?
begin
uri = URI.parse(url)
# Only allow HTTP/HTTPS schemes
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
# Only allow HTTPS in production
return nil unless Rails.env.development? || uri.scheme == 'https'
redirect_domain = uri.host.downcase
return nil unless redirect_domain.present?
# Check against our ForwardAuthRules
matching_rule = ForwardAuthRule.active.find do |rule|
rule.matches_domain?(redirect_domain)
end
matching_rule ? url : nil
rescue URI::InvalidURIError
nil
end
end
end