Improve finding the requested host's domain for setting the domain cookie
This commit is contained in:
3
Gemfile
3
Gemfile
@@ -31,6 +31,9 @@ gem "rqrcode", "~> 3.1"
|
|||||||
# JWT for OIDC ID tokens
|
# JWT for OIDC ID tokens
|
||||||
gem "jwt", "~> 3.1"
|
gem "jwt", "~> 3.1"
|
||||||
|
|
||||||
|
# Public Suffix List for domain parsing
|
||||||
|
gem "public_suffix", "~> 6.0"
|
||||||
|
|
||||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ DEPENDENCIES
|
|||||||
kamal
|
kamal
|
||||||
letter_opener
|
letter_opener
|
||||||
propshaft
|
propshaft
|
||||||
|
public_suffix (~> 6.0)
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.1.0)
|
rails (~> 8.1.0)
|
||||||
rotp (~> 6.3)
|
rotp (~> 6.3)
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ Clinch sits in a sweet spot between two excellent open-source identity solutions
|
|||||||
### Sign In
|
### Sign In
|
||||||
[](docs/screenshots/1-signin.png)
|
[](docs/screenshots/1-signin.png)
|
||||||
|
|
||||||
### Sign In with Error
|
### Sign In with 2FA
|
||||||
[](docs/screenshots/2-signin.png)
|
[](docs/screenshots/2-signin.png)
|
||||||
|
|
||||||
### Users Management
|
### Users Management
|
||||||
[](docs/screenshots/3-users.png)
|
[](docs/screenshots/3-users.png)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
require 'uri'
|
require 'uri'
|
||||||
|
require 'public_suffix'
|
||||||
|
|
||||||
module Authentication
|
module Authentication
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
@@ -73,37 +74,43 @@ module Authentication
|
|||||||
cookies.delete(:session_id)
|
cookies.delete(:session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extract root domain for cross-subdomain cookies
|
# Extract root domain for cross-subdomain cookies in SSO forward_auth system.
|
||||||
|
#
|
||||||
|
# PURPOSE: Enables a single authentication session to work across multiple subdomains
|
||||||
|
# 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!
|
||||||
|
# When accessing services by IP, there are no subdomains to share cookies with,
|
||||||
|
# and setting a domain cookie would break authentication.
|
||||||
|
#
|
||||||
|
# Uses the Public Suffix List (industry standard maintained by Mozilla) to
|
||||||
|
# correctly handle complex domain patterns like co.uk, com.au, appspot.com, etc.
|
||||||
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# - clinch.aapamilne.com -> .aapamilne.com
|
# - app.example.com -> .example.com (enables cross-subdomain SSO)
|
||||||
# - app.example.co.uk -> .example.co.uk
|
# - api.example.co.uk -> .example.co.uk (handles complex TLDs)
|
||||||
# - localhost -> nil (no domain setting for local development)
|
# - myapp.appspot.com -> .myapp.appspot.com (handles platform domains)
|
||||||
|
# - localhost -> nil (local development, no domain cookie)
|
||||||
|
# - 192.168.1.1 -> nil (IP access, no domain cookie - prevents SSO breakage)
|
||||||
|
#
|
||||||
|
# @param host [String] The request host (may include port)
|
||||||
|
# @return [String, nil] Root domain with leading dot for cookies, or nil for no domain setting
|
||||||
def extract_root_domain(host)
|
def extract_root_domain(host)
|
||||||
return nil if host.blank? || host.match?(/^(localhost|127\.0\.0\.1|::1)$/)
|
return nil if host.blank? || host.match?(/^(localhost|127\.0\.0\.1|::1)$/)
|
||||||
|
|
||||||
# Split hostname into parts
|
# Strip port number for domain parsing
|
||||||
parts = host.split('.')
|
host_without_port = host.split(':').first
|
||||||
|
|
||||||
# For normal domains like example.com, we need at least 2 parts
|
# Check if it's an IP address - if so, don't set domain cookie
|
||||||
# For complex domains like co.uk, we need at least 3 parts
|
return nil if host_without_port.match?(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
|
||||||
return nil if parts.length < 2
|
|
||||||
|
|
||||||
# Extract root domain with leading dot for cross-subdomain cookies
|
# Use Public Suffix List for accurate domain parsing
|
||||||
if parts.length >= 3
|
domain = PublicSuffix.parse(host_without_port)
|
||||||
# Check if it's a known complex TLD
|
".#{domain.domain}"
|
||||||
complex_tlds = %w[co.uk com.au co.nz co.za co.jp]
|
rescue PublicSuffix::DomainInvalid
|
||||||
second_level = "#{parts[-2]}.#{parts[-1]}"
|
# Fallback for invalid domains or IPs
|
||||||
|
nil
|
||||||
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
|
end
|
||||||
|
|
||||||
# Create a one-time token for forward auth to handle the race condition
|
# Create a one-time token for forward auth to handle the race condition
|
||||||
|
|||||||
Reference in New Issue
Block a user