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