Files
clinch/app/controllers/concerns/authentication.rb
Dan Milne 94785dbfe7
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
Update docs. Implemented a one-time token to work around domain cookies not being immediately return by the browser. Reduce db queries on /api/verify requests.
2025-10-28 08:20:12 +11:00

140 lines
4.3 KiB
Ruby

require 'uri'
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to signin_path
end
def after_authentication_url
return_url = session[:return_to_after_authenticating]
final_url = session.delete(:return_to_after_authenticating) || root_url
final_url
end
def start_new_session_for(user)
user.update!(last_sign_in_at: Time.current)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
# Extract root domain for cross-subdomain cookies (required for forward auth)
domain = extract_root_domain(request.host)
cookie_options = {
value: session.id,
httponly: true,
same_site: :lax,
secure: Rails.env.production?
}
# 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
# Create a one-time token for immediate forward auth after authentication
# This solves the race condition where browser hasn't processed cookie yet
create_forward_auth_token(session)
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
# Extract root domain for cross-subdomain cookies
# Examples:
# - clinch.aapamilne.com -> .aapamilne.com
# - app.example.co.uk -> .example.co.uk
# - localhost -> nil (no domain setting for local development)
def extract_root_domain(host)
return nil if host.blank? || host.match?(/^(localhost|127\.0\.0\.1|::1)$/)
# Split hostname into parts
parts = host.split('.')
# For normal domains like example.com, we need at least 2 parts
# For complex domains like co.uk, we need at least 3 parts
return nil if parts.length < 2
# Extract root domain with leading dot for cross-subdomain cookies
if parts.length >= 3
# Check if it's a known complex TLD
complex_tlds = %w[co.uk com.au co.nz co.za co.jp]
second_level = "#{parts[-2]}.#{parts[-1]}"
if complex_tlds.include?(second_level)
# For complex TLDs, include more parts: app.example.co.uk -> .example.co.uk
root_parts = parts[-3..-1]
return ".#{root_parts.join('.')}"
end
end
# For regular domains: app.example.com -> .example.com
root_parts = parts[-2..-1]
".#{root_parts.join('.')}"
end
# Create a one-time token for forward auth to handle the race condition
# where the browser hasn't processed the session cookie yet
def create_forward_auth_token(session_obj)
# Generate a secure random token
token = SecureRandom.urlsafe_base64(32)
# Store it with an expiry of 30 seconds
Rails.cache.write(
"forward_auth_token:#{token}",
session_obj.id,
expires_in: 30.seconds
)
# Set the token as a query parameter on the redirect URL
# We need to store this in the controller's session
controller_session = session
if controller_session[:return_to_after_authenticating].present?
original_url = controller_session[:return_to_after_authenticating]
uri = URI.parse(original_url)
# Add token as query parameter
query_params = URI.decode_www_form(uri.query || "").to_h
query_params['fa_token'] = token
uri.query = URI.encode_www_form(query_params)
# Update the session with the tokenized URL
controller_session[:return_to_after_authenticating] = uri.to_s
end
end
end