diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2023-09-29 08:46:41 +1000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-29 08:46:41 +1000 | 
| commit | 6a6059dc228b20fe13aee274188911d00458fe24 (patch) | |
| tree | 775d837651f575671afdef6b89536cf85f7dad33 /internal | |
| parent | 1a96b5c3c1997006a1830d9ca35ff806906f58b5 (diff) | |
feat(session): redirection by cookie domain (#6017)
This allows configuring the default redirection URL by session domain. In addition it makes the Authelia URL option in the new session config mandatory at least for the time being.
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
Diffstat (limited to 'internal')
32 files changed, 529 insertions, 271 deletions
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 2f9641776..cfb9585ea 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -27,15 +27,6 @@ theme: 'light'  ## set using a secret: https://www.authelia.com/c/secrets  jwt_secret: 'a_very_important_secret' -## Default redirection URL -## -## If user tries to authenticate without any referer, Authelia does not know where to redirect the user to at the end -## of the authentication process. This parameter allows you to specify the default redirection URL Authelia will use -## in such a case. -## -## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication. -# default_redirection_url: 'https://home.example.com/' -  ## Set the default 2FA method for new users and for when a user has a preferred method configured that has been  ## disabled. This setting must be a method that is enabled.  ## Options are totp, webauthn, mobile_push. @@ -708,14 +699,28 @@ session:        ## Note: the Authelia portal must also be in that domain.        # domain: 'example.com' -      ## Optional. The fully qualified URI of the portal to redirect users to on proxies that support redirections. +      ## Required. The fully qualified URI of the portal to redirect users to on proxies that support redirections.        ## Rules:        ##   - MUST use the secure scheme 'https://' -      ##   - The above domain MUST either: +      ##   - The above 'domain' option MUST either:        ##      - Match the host portion of this URI.        ##      - Match the suffix of the host portion when prefixed with '.'.        # authelia_url: 'https://auth.example.com' +      ## Optional. The fully qualified URI used as the redirection location if the portal is accessed directly. Not +      ## configuring this option disables the automatic redirection behaviour. +      ## +      ## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication +      ## unless they were redirected to Authelia by the proxy. +      ## +      ## Rules: +      ##   - MUST use the secure scheme 'https://' +      ##   - MUST not match the 'authelia_url' option. +      ##   - The above 'domain' option MUST either: +      ##      - Match the host portion of this URI. +      ##      - Match the suffix of the host portion when prefixed with '.'. +      # default_redirection_url: 'https://www.example.com' +        ## Sets the Cookie SameSite value. Possible options are none, lax, or strict.        ## Please read https://www.authelia.com/c/session#same_site        # same_site: 'lax' diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index 3761aad3d..3fdd09f79 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -1,12 +1,16 @@  package schema +import ( +	"net/url" +) +  // Configuration object extracted from YAML configuration file.  type Configuration struct { -	Theme                 string `koanf:"theme" json:"theme" jsonschema:"default=light,enum=auto,enum=light,enum=dark,enum=grey,title=Theme Name" jsonschema_description:"The name of the theme to apply to the web UI"` -	CertificatesDirectory string `koanf:"certificates_directory" json:"certificates_directory" jsonschema:"title=Certificates Directory Path" jsonschema_description:"The path to a directory which is used to determine the certificates that are trusted"` -	JWTSecret             string `koanf:"jwt_secret" json:"jwt_secret" jsonschema:"title=Secret Key for JWT's" jsonschema_description:"Used for signing HS256 JWT's for identity verification"` -	DefaultRedirectionURL string `koanf:"default_redirection_url" json:"default_redirection_url" jsonschema:"title=The default redirection URL" jsonschema_description:"Used to redirect users when they visit the portal directly"` -	Default2FAMethod      string `koanf:"default_2fa_method" json:"default_2fa_method" jsonschema:"enum=totp,enum=webauthn,enum=mobile_push,title=Default 2FA method" jsonschema_description:"When a user logs in for the first time this is the 2FA method configured for them"` +	Theme                 string   `koanf:"theme" json:"theme" jsonschema:"default=light,enum=auto,enum=light,enum=dark,enum=grey,title=Theme Name" jsonschema_description:"The name of the theme to apply to the web UI"` +	CertificatesDirectory string   `koanf:"certificates_directory" json:"certificates_directory" jsonschema:"title=Certificates Directory Path" jsonschema_description:"The path to a directory which is used to determine the certificates that are trusted"` +	JWTSecret             string   `koanf:"jwt_secret" json:"jwt_secret" jsonschema:"title=Secret Key for JWT's" jsonschema_description:"Used for signing HS256 JWT's for identity verification"` +	DefaultRedirectionURL *url.URL `koanf:"default_redirection_url" json:"default_redirection_url" jsonschema:"format=uri,title=The default redirection URL" jsonschema_description:"Used to redirect users when they visit the portal directly"` +	Default2FAMethod      string   `koanf:"default_2fa_method" json:"default_2fa_method" jsonschema:"enum=totp,enum=webauthn,enum=mobile_push,title=Default 2FA method" jsonschema_description:"When a user logs in for the first time this is the 2FA method configured for them"`  	Log                   Log                   `koanf:"log" json:"log" jsonschema:"title=Log" jsonschema_description:"Logging Configuration"`  	IdentityProviders     IdentityProviders     `koanf:"identity_providers" json:"identity_providers" jsonschema:"title=Identity Providers" jsonschema_description:"Identity Providers Configuration"` diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index 7d0ee6c68..ed755626f 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -188,6 +188,8 @@ var Keys = []string{  	"session.cookies[]",  	"session.cookies[].domain",  	"session.cookies[].authelia_url", +	"session.cookies[].default_redirection_url", +	"session.cookies[]",  	"session.redis.host",  	"session.redis.port",  	"session.redis.username", diff --git a/internal/configuration/schema/session.go b/internal/configuration/schema/session.go index 36048051a..71d66e916 100644 --- a/internal/configuration/schema/session.go +++ b/internal/configuration/schema/session.go @@ -27,15 +27,18 @@ type SessionCookieCommon struct {  	Inactivity time.Duration `koanf:"inactivity" json:"inactivity" jsonschema:"default=5 minutes"`  	RememberMe time.Duration `koanf:"remember_me" json:"remember_me" jsonschema:"default=30 days"` -	DisableRememberMe bool +	DisableRememberMe bool `json:"-"`  }  // SessionCookie represents the configuration for a cookie domain.  type SessionCookie struct {  	SessionCookieCommon `koanf:",squash"` -	Domain      string   `koanf:"domain" json:"domain" jsonschema:"format=hostname,title=Domain" jsonschema_description:"The domain for this session cookie"` -	AutheliaURL *url.URL `koanf:"authelia_url" json:"authelia_url" jsonschema:"format=uri,title=Authelia URL" jsonschema_description:"The Root Authelia URL to redirect users to for this session cookie"` +	Domain                string   `koanf:"domain" json:"domain" jsonschema:"format=hostname,title=Domain" jsonschema_description:"The domain for this session cookie"` +	AutheliaURL           *url.URL `koanf:"authelia_url" json:"authelia_url" jsonschema:"format=uri,title=Authelia URL" jsonschema_description:"The Root Authelia URL to redirect users to for this session cookie"` +	DefaultRedirectionURL *url.URL `koanf:"default_redirection_url" json:"default_redirection_url" jsonschema:"format=uri,title=Default Redirection URL" jsonschema_description:"The default redirection URL for this cookie domain"` + +	Legacy bool `json:"-"`  }  // SessionRedis represents the configuration related to redis session store. diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go index 7c4969a97..be23104ba 100644 --- a/internal/configuration/schema/types.go +++ b/internal/configuration/schema/types.go @@ -19,7 +19,7 @@ import (  	"github.com/go-crypt/crypt/algorithm"  	"github.com/go-crypt/crypt/algorithm/plaintext"  	"github.com/valyala/fasthttp" -	yaml "gopkg.in/yaml.v3" +	"gopkg.in/yaml.v3"  )  var cdecoder algorithm.DecoderRegister diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index df3d5e328..e7e45fbfb 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -3,7 +3,6 @@ package validator  import (  	"fmt"  	"os" -	"strings"  	"github.com/authelia/authelia/v4/internal/configuration/schema"  	"github.com/authelia/authelia/v4/internal/utils" @@ -27,10 +26,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc  		validator.Push(fmt.Errorf("option 'jwt_secret' is required"))  	} -	if config.DefaultRedirectionURL != "" { -		if err = utils.IsStringAbsURL(config.DefaultRedirectionURL); err != nil { -			validator.Push(fmt.Errorf("option 'default_redirection_url' is invalid: %s", strings.ReplaceAll(err.Error(), "like 'http://' or 'https://'", "like 'ldap://' or 'ldaps://'"))) -		} +	if config.DefaultRedirectionURL != nil && !config.DefaultRedirectionURL.IsAbs() { +		validator.Push(fmt.Errorf("option 'default_redirection_url' is invalid: the url '%s' is not absolute", config.DefaultRedirectionURL.String()))  	}  	validateDefault2FAMethod(config, validator) @@ -51,7 +48,7 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc  	ValidateRules(config, validator) -	ValidateSession(&config.Session, validator) +	ValidateSession(config, validator)  	ValidateRegulation(config, validator) diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index 6054e53c8..984aaf9f6 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -2,6 +2,7 @@ package validator  import (  	"fmt" +	"net/url"  	"runtime"  	"testing" @@ -30,7 +31,8 @@ func newDefaultConfig() schema.Configuration {  				SessionCookieCommon: schema.SessionCookieCommon{  					Name: "authelia_session",  				}, -				Domain: exampleDotCom, +				Domain:      exampleDotCom, +				AutheliaURL: &url.URL{Scheme: "https", Host: "auth." + exampleDotCom},  			},  		},  	} @@ -100,13 +102,13 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) {  func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultConfig() -	config.DefaultRedirectionURL = "bad_default_redirection_url" +	config.DefaultRedirectionURL = &url.URL{Host: "localhost"}  	ValidateConfiguration(&config, validator)  	require.Len(t, validator.Errors(), 1)  	require.Len(t, validator.Warnings(), 1) -	assert.EqualError(t, validator.Errors()[0], "option 'default_redirection_url' is invalid: could not parse 'bad_default_redirection_url' as a URL") +	assert.EqualError(t, validator.Errors()[0], "option 'default_redirection_url' is invalid: the url '//localhost' is not absolute")  	assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")  } diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index fe5338563..01b7cd065 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -322,17 +322,19 @@ const (  	errFmtSessionRedisSentinelMissingName     = "session: redis: high_availability: option 'sentinel_name' is required"  	errFmtSessionRedisSentinelNodeHostMissing = "session: redis: high_availability: option 'nodes': option 'host' is required for each node but one or more nodes are missing this" -	errFmtSessionDomainMustBeRoot                = "session: domain config %s: option 'domain' must be the domain you wish to protect not a wildcard domain but it's configured as '%s'" -	errFmtSessionDomainSameSite                  = "session: domain config %s: option 'same_site' must be one of %s but it's configured as '%s'" -	errFmtSessionDomainRequired                  = "session: domain config %s: option 'domain' is required" -	errFmtSessionDomainHasPeriodPrefix           = "session: domain config %s: option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it" -	errFmtSessionDomainDuplicate                 = "session: domain config %s: option 'domain' is a duplicate value for another configured session domain" -	errFmtSessionDomainDuplicateCookieScope      = "session: domain config %s: option 'domain' shares the same cookie domain scope as another configured session domain" -	errFmtSessionDomainPortalURLInsecure         = "session: domain config %s: option 'authelia_url' does not have a secure scheme with a value of '%s'" -	errFmtSessionDomainPortalURLNotInCookieScope = "session: domain config %s: option 'authelia_url' does not share a cookie scope with domain '%s' with a value of '%s'" -	errFmtSessionDomainInvalidDomain             = "session: domain config %s: option 'domain' does not appear to be a valid cookie domain or an ip address" -	errFmtSessionDomainInvalidDomainNoDots       = "session: domain config %s: option 'domain' is not a valid cookie domain: must have at least a single period or be an ip address" -	errFmtSessionDomainInvalidDomainPublic       = "session: domain config %s: option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list" +	errFmtSessionDomainMustBeRoot                        = "session: domain config %s: option 'domain' must be the domain you wish to protect not a wildcard domain but it's configured as '%s'" +	errFmtSessionDomainSameSite                          = "session: domain config %s: option 'same_site' must be one of %s but it's configured as '%s'" +	errFmtSessionDomainOptionRequired                    = "session: domain config %s: option '%s' is required" +	errFmtSessionDomainHasPeriodPrefix                   = "session: domain config %s: option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it" +	errFmtSessionDomainDuplicate                         = "session: domain config %s: option 'domain' is a duplicate value for another configured session domain" +	errFmtSessionDomainDuplicateCookieScope              = "session: domain config %s: option 'domain' shares the same cookie domain scope as another configured session domain" +	errFmtSessionDomainURLNotAbsolute                    = "session: domain config %s: option '%s' is not absolute with a value of '%s'" +	errFmtSessionDomainURLInsecure                       = "session: domain config %s: option '%s' does not have a secure scheme with a value of '%s'" +	errFmtSessionDomainURLNotInCookieScope               = "session: domain config %s: option '%s' does not share a cookie scope with domain '%s' with a value of '%s'" +	errFmtSessionDomainAutheliaURLAndRedirectionURLEqual = "session: domain config %s: option 'default_redirection_url' with value '%s' is effectively equal to option 'authelia_url' with value '%s' which is not permitted" +	errFmtSessionDomainInvalidDomain                     = "session: domain config %s: option 'domain' does not appear to be a valid cookie domain or an ip address" +	errFmtSessionDomainInvalidDomainNoDots               = "session: domain config %s: option 'domain' is not a valid cookie domain: must have at least a single period or be an ip address" +	errFmtSessionDomainInvalidDomainPublic               = "session: domain config %s: option 'domain' is not a valid cookie domain: the domain is part of the special public suffix list"  )  // Regulation Error Consts. @@ -482,6 +484,9 @@ const (  	attrOIDCAccessTokenSigKID     = "access_token_signed_response_key_id"  	attrOIDCPKCEChallengeMethod   = "pkce_challenge_method"  	attrOIDCRequestedAudienceMode = "requested_audience_mode" +	attrSessionAutheliaURL        = "authelia_url" +	attrSessionDomain             = "domain" +	attrDefaultRedirectionURL     = "default_redirection_url"  )  var ( diff --git a/internal/configuration/validator/session.go b/internal/configuration/validator/session.go index be0c28a44..24fcc54d5 100644 --- a/internal/configuration/validator/session.go +++ b/internal/configuration/validator/session.go @@ -11,65 +11,67 @@ import (  )  // ValidateSession validates and update session configuration. -func ValidateSession(config *schema.Session, validator *schema.StructValidator) { -	if config.Name == "" { -		config.Name = schema.DefaultSessionConfiguration.Name +func ValidateSession(config *schema.Configuration, validator *schema.StructValidator) { +	if config.Session.Name == "" { +		config.Session.Name = schema.DefaultSessionConfiguration.Name  	} -	if config.Redis != nil { -		if config.Redis.HighAvailability != nil { -			validateRedisSentinel(config, validator) +	if config.Session.Redis != nil { +		if config.Session.Redis.HighAvailability != nil { +			validateRedisSentinel(&config.Session, validator)  		} else { -			validateRedis(config, validator) +			validateRedis(&config.Session, validator)  		}  	}  	validateSession(config, validator)  } -func validateSession(config *schema.Session, validator *schema.StructValidator) { -	if config.Expiration <= 0 { -		config.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour. +func validateSession(config *schema.Configuration, validator *schema.StructValidator) { +	if config.Session.Expiration <= 0 { +		config.Session.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour.  	} -	if config.Inactivity <= 0 { -		config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. +	if config.Session.Inactivity <= 0 { +		config.Session.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min.  	}  	switch { -	case config.RememberMe == schema.RememberMeDisabled: -		config.DisableRememberMe = true -	case config.RememberMe <= 0: -		config.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month. +	case config.Session.RememberMe == schema.RememberMeDisabled: +		config.Session.DisableRememberMe = true +	case config.Session.RememberMe <= 0: +		config.Session.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month.  	} -	if config.SameSite == "" { -		config.SameSite = schema.DefaultSessionConfiguration.SameSite -	} else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) { -		validator.Push(fmt.Errorf(errFmtSessionSameSite, strJoinOr(validSessionSameSiteValues), config.SameSite)) +	if config.Session.SameSite == "" { +		config.Session.SameSite = schema.DefaultSessionConfiguration.SameSite +	} else if !utils.IsStringInSlice(config.Session.SameSite, validSessionSameSiteValues) { +		validator.Push(fmt.Errorf(errFmtSessionSameSite, strJoinOr(validSessionSameSiteValues), config.Session.SameSite))  	} -	cookies := len(config.Cookies) +	cookies := len(config.Session.Cookies)  	switch { -	case cookies == 0 && config.Domain != "": //nolint:staticcheck +	case cookies == 0 && config.Session.Domain != "": //nolint:staticcheck  		// Add legacy configuration to the domains list. -		config.Cookies = append(config.Cookies, schema.SessionCookie{ +		config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  			SessionCookieCommon: schema.SessionCookieCommon{ -				Name:              config.Name, -				SameSite:          config.SameSite, -				Expiration:        config.Expiration, -				Inactivity:        config.Inactivity, -				RememberMe:        config.RememberMe, -				DisableRememberMe: config.DisableRememberMe, +				Name:              config.Session.Name, +				SameSite:          config.Session.SameSite, +				Expiration:        config.Session.Expiration, +				Inactivity:        config.Session.Inactivity, +				RememberMe:        config.Session.RememberMe, +				DisableRememberMe: config.Session.DisableRememberMe,  			}, -			Domain: config.Domain, //nolint:staticcheck +			Domain:                config.Session.Domain, //nolint:staticcheck +			DefaultRedirectionURL: config.DefaultRedirectionURL, +			Legacy:                true,  		}) -	case cookies != 0 && config.Domain != "": //nolint:staticcheck +	case cookies != 0 && config.Session.Domain != "": //nolint:staticcheck  		validator.Push(fmt.Errorf(errFmtSessionLegacyAndWarning))  	} -	validateSessionCookieDomains(config, validator) +	validateSessionCookieDomains(&config.Session, validator)  }  func validateSessionCookieDomains(config *schema.Session, validator *schema.StructValidator) { @@ -86,7 +88,7 @@ func validateSessionCookieDomains(config *schema.Session, validator *schema.Stru  		validateSessionCookieName(i, config) -		validateSessionCookiesAutheliaURL(i, config, validator) +		validateSessionCookiesURLs(i, config, validator)  		validateSessionExpiration(i, config) @@ -104,7 +106,7 @@ func validateSessionDomainName(i int, config *schema.Session, validator *schema.  	switch {  	case d.Domain == "": -		validator.Push(fmt.Errorf(errFmtSessionDomainRequired, sessionDomainDescriptor(i, d))) +		validator.Push(fmt.Errorf(errFmtSessionDomainOptionRequired, sessionDomainDescriptor(i, d), attrSessionDomain))  		return  	case strings.HasPrefix(d.Domain, "*."):  		validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain)) @@ -154,15 +156,39 @@ func validateSessionUniqueCookieDomain(i int, config *schema.Session, domains []  	}  } -// validateSessionCookiesAutheliaURL validates the AutheliaURL. -func validateSessionCookiesAutheliaURL(index int, config *schema.Session, validator *schema.StructValidator) { -	var d = config.Cookies[index] +// validateSessionCookiesURLs validates the AutheliaURL and DefaultRedirectionURL. +func validateSessionCookiesURLs(i int, config *schema.Session, validator *schema.StructValidator) { +	var d = config.Cookies[i] -	if d.AutheliaURL != nil && d.Domain != "" && !utils.IsURISafeRedirection(d.AutheliaURL, d.Domain) { -		if utils.IsURISecure(d.AutheliaURL) { -			validator.Push(fmt.Errorf(errFmtSessionDomainPortalURLNotInCookieScope, sessionDomainDescriptor(index, d), d.Domain, d.AutheliaURL)) -		} else { -			validator.Push(fmt.Errorf(errFmtSessionDomainPortalURLInsecure, sessionDomainDescriptor(index, d), d.AutheliaURL)) +	if d.AutheliaURL == nil { +		if !d.Legacy && d.Domain != "" { +			validator.Push(fmt.Errorf(errFmtSessionDomainOptionRequired, sessionDomainDescriptor(i, d), attrSessionAutheliaURL)) +		} +	} else { +		if !d.AutheliaURL.IsAbs() { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotAbsolute, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.AutheliaURL)) +		} else if !utils.IsURISecure(d.AutheliaURL) { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLInsecure, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.AutheliaURL)) +		} + +		if d.Domain != "" && !utils.HasURIDomainSuffix(d.AutheliaURL, d.Domain) { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotInCookieScope, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.Domain, d.AutheliaURL)) +		} +	} + +	if d.DefaultRedirectionURL != nil { +		if !d.DefaultRedirectionURL.IsAbs() { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotAbsolute, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.DefaultRedirectionURL)) +		} else if !utils.IsURISecure(d.DefaultRedirectionURL) { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLInsecure, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.DefaultRedirectionURL)) +		} + +		if d.Domain != "" && !utils.HasURIDomainSuffix(d.DefaultRedirectionURL, d.Domain) { +			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotInCookieScope, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.Domain, d.DefaultRedirectionURL)) +		} + +		if d.AutheliaURL != nil && utils.EqualURLs(d.AutheliaURL, d.DefaultRedirectionURL) { +			validator.Push(fmt.Errorf(errFmtSessionDomainAutheliaURLAndRedirectionURLEqual, sessionDomainDescriptor(i, d), d.DefaultRedirectionURL, d.AutheliaURL))  		}  	}  } diff --git a/internal/configuration/validator/session_test.go b/internal/configuration/validator/session_test.go index e592a5223..ed8121610 100644 --- a/internal/configuration/validator/session_test.go +++ b/internal/configuration/validator/session_test.go @@ -13,13 +13,13 @@ import (  	"github.com/authelia/authelia/v4/internal/configuration/schema"  ) -func newDefaultSessionConfig() schema.Session { +func newDefaultSessionConfig() schema.Configuration {  	config := schema.Session{}  	config.Secret = testJWTSecret  	config.Domain = exampleDotCom //nolint:staticcheck  	config.Cookies = []schema.SessionCookie{} -	return config +	return schema.Configuration{Session: config}  }  func TestShouldSetDefaultSessionValues(t *testing.T) { @@ -30,40 +30,45 @@ func TestShouldSetDefaultSessionValues(t *testing.T) {  	assert.False(t, validator.HasWarnings())  	assert.False(t, validator.HasErrors()) -	assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name) -	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) -	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) -	assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe) -	assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite) +	assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Session.Name) +	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Session.Inactivity) +	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Session.Expiration) +	assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.Session.RememberMe) +	assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.Session.SameSite)  }  func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {  	testCases := []struct {  		name     string -		have     schema.Session -		expected schema.Session +		have     schema.Configuration +		expected schema.Configuration  		errs     []string  	}{  		{  			"ShouldSetGoodDefaultValues", -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +					}, +					Domain: exampleDotCom,  				}, -				Domain: exampleDotCom,  			}, -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, -				}, -				Domain: exampleDotCom, -				Cookies: []schema.SessionCookie{ -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, -							Inactivity: time.Minute, RememberMe: time.Hour * 2, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +					}, +					Domain: exampleDotCom, +					Cookies: []schema.SessionCookie{ +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, +								Inactivity: time.Minute, RememberMe: time.Hour * 2, +							}, +							Domain: exampleDotCom, +							Legacy: true,  						}, -						Domain: exampleDotCom,  					},  				},  			}, @@ -71,31 +76,37 @@ func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {  		},  		{  			"ShouldNotSetBadDefaultValues", -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, -				}, -				Cookies: []schema.SessionCookie{ -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name:       "authelia_session", -							Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +					}, +					Cookies: []schema.SessionCookie{ +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name:       "authelia_session", +								Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +							}, +							Domain:      exampleDotCom, +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth." + exampleDotCom},  						}, -						Domain: exampleDotCom,  					},  				},  			}, -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "authelia_session", SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, -				}, -				Cookies: []schema.SessionCookie{ -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name: "authelia_session", SameSite: schema.DefaultSessionConfiguration.SameSite, -							Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "authelia_session", SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +					}, +					Cookies: []schema.SessionCookie{ +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name: "authelia_session", SameSite: schema.DefaultSessionConfiguration.SameSite, +								Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, +							}, +							Domain:      exampleDotCom, +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth." + exampleDotCom},  						}, -						Domain: exampleDotCom,  					},  				},  			}, @@ -105,42 +116,50 @@ func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {  		},  		{  			"ShouldSetDefaultValuesForEachConfig", -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, -					RememberMe: schema.RememberMeDisabled, -				}, -				Cookies: []schema.SessionCookie{ -					{ -						Domain: exampleDotCom, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, +						RememberMe: schema.RememberMeDisabled,  					}, -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name: "authelia_session", SameSite: "strict", +					Cookies: []schema.SessionCookie{ +						{ +							Domain:      exampleDotCom, +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth." + exampleDotCom}, +						}, +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name: "authelia_session", SameSite: "strict", +							}, +							Domain:      "example2.com", +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth.example2.com"},  						}, -						Domain: "example2.com",  					},  				},  			}, -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, -					RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, -				}, -				Cookies: []schema.SessionCookie{ -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name: "default_session", SameSite: "lax", -							Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, -						}, -						Domain: exampleDotCom, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, +						RememberMe: schema.RememberMeDisabled, DisableRememberMe: true,  					}, -					{ -						SessionCookieCommon: schema.SessionCookieCommon{ -							Name: "authelia_session", SameSite: "strict", -							Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, +					Cookies: []schema.SessionCookie{ +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name: "default_session", SameSite: "lax", +								Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, +							}, +							Domain:      exampleDotCom, +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth." + exampleDotCom}, +						}, +						{ +							SessionCookieCommon: schema.SessionCookieCommon{ +								Name: "authelia_session", SameSite: "strict", +								Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, +							}, +							Domain:      "example2.com", +							AutheliaURL: &url.URL{Scheme: "https", Host: "auth.example2.com"},  						}, -						Domain: "example2.com",  					},  				},  			}, @@ -148,18 +167,22 @@ func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {  		},  		{  			"ShouldErrorOnEmptyConfig", -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "", SameSite: "", +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "", SameSite: "", +					}, +					Domain:  "", +					Cookies: []schema.SessionCookie{},  				}, -				Domain:  "", -				Cookies: []schema.SessionCookie{},  			}, -			schema.Session{ -				SessionCookieCommon: schema.SessionCookieCommon{ -					Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute * 5, RememberMe: time.Hour * 24 * 30, +			schema.Configuration{ +				Session: schema.Session{ +					SessionCookieCommon: schema.SessionCookieCommon{ +						Name: "authelia_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute * 5, RememberMe: time.Hour * 24 * 30, +					}, +					Cookies: []schema.SessionCookie{},  				}, -				Cookies: []schema.SessionCookie{},  			},  			[]string{  				"session: option 'cookies' is required", @@ -186,7 +209,7 @@ func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {  				assert.EqualError(t, err, tc.errs[i])  			} -			assert.Equal(t, tc.expected, have) +			assert.Equal(t, tc.expected.Session, have.Session)  		})  	}  } @@ -195,22 +218,22 @@ func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Expiration, config.Inactivity, config.RememberMe = -1, -1, -2 +	config.Session.Expiration, config.Session.Inactivity, config.Session.RememberMe = -1, -1, -2  	ValidateSession(&config, validator)  	assert.Len(t, validator.Warnings(), 0)  	assert.Len(t, validator.Errors(), 0) -	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) -	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) -	assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe) +	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Session.Inactivity) +	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Session.Expiration) +	assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.Session.RememberMe)  }  func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Domain = ".example.com" //nolint:staticcheck +	config.Session.Domain = ".example.com" //nolint:staticcheck  	ValidateSession(&config, validator) @@ -232,7 +255,7 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {  	config = newDefaultSessionConfig()  	// Set redis config because password must be set only when redis is used. -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host:     "redis.localhost",  		Port:     6379,  		Password: "password", @@ -243,14 +266,14 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {  	assert.Len(t, validator.Warnings(), 0)  	assert.Len(t, validator.Errors(), 0) -	assert.Equal(t, 8, config.Redis.MaximumActiveConnections) +	assert.Equal(t, 8, config.Session.Redis.MaximumActiveConnections)  }  func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "authelia-port-1",  		Port: -1,  	} @@ -267,7 +290,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "authelia-port-1",  		Port: 65536,  	} @@ -283,7 +306,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) {  func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Secret = "" +	config.Session.Secret = ""  	ValidateSession(&config, validator) @@ -291,10 +314,10 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {  	validator.Clear()  	config = newDefaultSessionConfig() -	config.Secret = "" +	config.Session.Secret = ""  	// Set redis config because password must be set only when redis is used. -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.localhost",  		Port: 6379,  	} @@ -318,7 +341,7 @@ func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisPortBlank(t *testing.T) {  	config = newDefaultSessionConfig()  	// Set redis config because password must be set only when redis is used. -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.localhost",  		Port: 0,  	} @@ -328,7 +351,7 @@ func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisPortBlank(t *testing.T) {  	assert.False(t, validator.HasWarnings())  	assert.False(t, validator.HasErrors()) -	assert.Equal(t, 6379, config.Redis.Port) +	assert.Equal(t, 6379, config.Session.Redis.Port)  }  func TestShouldRaiseErrorWhenRedisPortInvalid(t *testing.T) { @@ -343,7 +366,7 @@ func TestShouldRaiseErrorWhenRedisPortInvalid(t *testing.T) {  	config = newDefaultSessionConfig()  	// Set redis config because password must be set only when redis is used. -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.localhost",  		Port: -1,  	} @@ -359,7 +382,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityHasNodesWithNoHost(t *testi  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis",  		Port: 6379,  		HighAvailability: &schema.SessionRedisHighAvailability{ @@ -390,7 +413,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t *  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis",  		Port: 6379,  		HighAvailability: &schema.SessionRedisHighAvailability{ @@ -412,7 +435,7 @@ func TestShouldUpdateDefaultPortWhenRedisSentinelHasNodes(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis",  		Port: 6379,  		HighAvailability: &schema.SessionRedisHighAvailability{ @@ -438,17 +461,17 @@ func TestShouldUpdateDefaultPortWhenRedisSentinelHasNodes(t *testing.T) {  	assert.False(t, validator.HasWarnings())  	assert.False(t, validator.HasErrors()) -	assert.Equal(t, 333, config.Redis.HighAvailability.Nodes[0].Port) -	assert.Equal(t, 26379, config.Redis.HighAvailability.Nodes[1].Port) -	assert.Equal(t, 26379, config.Redis.HighAvailability.Nodes[2].Port) +	assert.Equal(t, 333, config.Session.Redis.HighAvailability.Nodes[0].Port) +	assert.Equal(t, 26379, config.Session.Redis.HighAvailability.Nodes[1].Port) +	assert.Equal(t, 26379, config.Session.Redis.HighAvailability.Nodes[2].Port)  }  func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Secret = "" -	config.Redis = &schema.SessionRedis{ +	config.Session.Secret = "" +	config.Session.Redis = &schema.SessionRedis{  		Port: 65536,  		HighAvailability: &schema.SessionRedisHighAvailability{  			SentinelName:     "sentinel", @@ -478,8 +501,8 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi  	config = newDefaultSessionConfig() -	config.Secret = "" -	config.Redis = &schema.SessionRedis{ +	config.Session.Secret = "" +	config.Session.Redis = &schema.SessionRedis{  		Port: -1,  		HighAvailability: &schema.SessionRedisHighAvailability{  			SentinelName:     "sentinel", @@ -510,7 +533,7 @@ func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *test  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "mysentinelHost",  		Port: 0,  		HighAvailability: &schema.SessionRedisHighAvailability{ @@ -532,14 +555,14 @@ func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *test  	assert.False(t, validator.HasWarnings())  	assert.False(t, validator.HasErrors()) -	assert.Equal(t, 26379, config.Redis.Port) +	assert.Equal(t, 26379, config.Session.Redis.Port)  }  func TestShouldRaiseErrorWhenRedisHostAndHighAvailabilityNodesEmpty(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Port: 26379,  		HighAvailability: &schema.SessionRedisHighAvailability{  			SentinelName:     "sentinel", @@ -561,7 +584,7 @@ func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Port: 6379,  	} @@ -579,7 +602,7 @@ func TestShouldSetDefaultRedisTLSOptions(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.local",  		Port: 6379,  		TLS:  &schema.TLS{}, @@ -590,16 +613,16 @@ func TestShouldSetDefaultRedisTLSOptions(t *testing.T) {  	assert.Len(t, validator.Warnings(), 0)  	assert.Len(t, validator.Errors(), 0) -	assert.Equal(t, uint16(tls.VersionTLS12), config.Redis.TLS.MinimumVersion.Value) -	assert.Equal(t, uint16(0), config.Redis.TLS.MaximumVersion.Value) -	assert.Equal(t, "redis.local", config.Redis.TLS.ServerName) +	assert.Equal(t, uint16(tls.VersionTLS12), config.Session.Redis.TLS.MinimumVersion.Value) +	assert.Equal(t, uint16(0), config.Session.Redis.TLS.MaximumVersion.Value) +	assert.Equal(t, "redis.local", config.Session.Redis.TLS.ServerName)  }  func TestShouldRaiseErrorOnBadRedisTLSOptionsSSL30(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.local",  		Port: 6379,  		TLS: &schema.TLS{ @@ -619,7 +642,7 @@ func TestShouldRaiseErrorOnBadRedisTLSOptionsMinVerGreaterThanMax(t *testing.T)  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Redis = &schema.SessionRedis{ +	config.Session.Redis = &schema.SessionRedis{  		Host: "redis.local",  		Port: 6379,  		TLS: &schema.TLS{ @@ -639,38 +662,111 @@ func TestShouldRaiseErrorOnBadRedisTLSOptionsMinVerGreaterThanMax(t *testing.T)  func TestShouldRaiseErrorWhenHaveDuplicatedDomainName(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Domain = "" //nolint:staticcheck -	config.Cookies = append(config.Cookies, schema.SessionCookie{ +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  		Domain:      exampleDotCom,  		AutheliaURL: MustParseURL("https://login.example.com"),  	}) -	config.Cookies = append(config.Cookies, schema.SessionCookie{ +	config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  		Domain:      exampleDotCom,  		AutheliaURL: MustParseURL("https://login.example.com"),  	})  	ValidateSession(&config, validator)  	assert.False(t, validator.HasWarnings()) -	assert.Len(t, validator.Errors(), 1) -	assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(1, schema.SessionCookie{Domain: exampleDotCom}))) +	require.Len(t, validator.Errors(), 1) +	assert.EqualError(t, validator.Errors()[0], "session: domain config #2 (domain 'example.com'): option 'domain' is a duplicate value for another configured session domain") +} + +func TestShouldRaiseErrorWhenHaveNonAbsAutheliaURL(t *testing.T) { +	validator := schema.NewStructValidator() +	config := newDefaultSessionConfig() +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = []schema.SessionCookie{ +		{ +			Domain:      exampleDotCom, +			AutheliaURL: MustParseURL("login.example.com"), +		}, +	} + +	ValidateSession(&config, validator) +	assert.False(t, validator.HasWarnings()) +	require.Len(t, validator.Errors(), 2) +	assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example.com'): option 'authelia_url' is not absolute with a value of 'login.example.com'") +	assert.EqualError(t, validator.Errors()[1], "session: domain config #1 (domain 'example.com'): option 'authelia_url' does not share a cookie scope with domain 'example.com' with a value of 'login.example.com'") +} + +func TestShouldRaiseErrorWhenHaveNonAbsDefaultRedirectionURL(t *testing.T) { +	validator := schema.NewStructValidator() +	config := newDefaultSessionConfig() +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = []schema.SessionCookie{ +		{ +			Domain:                exampleDotCom, +			AutheliaURL:           MustParseURL("https://login.example.com"), +			DefaultRedirectionURL: MustParseURL("home.example.com"), +		}, +	} + +	ValidateSession(&config, validator) +	assert.False(t, validator.HasWarnings()) +	require.Len(t, validator.Errors(), 2) +	assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example.com'): option 'default_redirection_url' is not absolute with a value of 'home.example.com'") +	assert.EqualError(t, validator.Errors()[1], "session: domain config #1 (domain 'example.com'): option 'default_redirection_url' does not share a cookie scope with domain 'example.com' with a value of 'home.example.com'") +} + +func TestShouldRaiseErrorWhenHaveNonSecureDefaultRedirectionURL(t *testing.T) { +	validator := schema.NewStructValidator() +	config := newDefaultSessionConfig() +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = []schema.SessionCookie{ +		{ +			Domain:                exampleDotCom, +			AutheliaURL:           MustParseURL("https://login.example.com"), +			DefaultRedirectionURL: MustParseURL("http://home.example.com"), +		}, +	} + +	ValidateSession(&config, validator) +	assert.False(t, validator.HasWarnings()) +	require.Len(t, validator.Errors(), 1) +	assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example.com'): option 'default_redirection_url' does not have a secure scheme with a value of 'http://home.example.com'") +} + +func TestShouldRaiseErrorWhenHaveDefaultRedirectionURLEqualAutheliaURL(t *testing.T) { +	validator := schema.NewStructValidator() +	config := newDefaultSessionConfig() +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = []schema.SessionCookie{ +		{ +			Domain:                exampleDotCom, +			AutheliaURL:           MustParseURL("https://login.example.com"), +			DefaultRedirectionURL: MustParseURL("https://login.example.com"), +		}, +	} + +	ValidateSession(&config, validator) +	assert.False(t, validator.HasWarnings()) +	require.Len(t, validator.Errors(), 1) +	assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example.com'): option 'default_redirection_url' with value 'https://login.example.com' is effectively equal to option 'authelia_url' with value 'https://login.example.com' which is not permitted")  }  func TestShouldRaiseErrorWhenSubdomainConflicts(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Domain = "" //nolint:staticcheck -	config.Cookies = append(config.Cookies, schema.SessionCookie{ +	config.Session.Domain = "" //nolint:staticcheck +	config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  		Domain:      exampleDotCom,  		AutheliaURL: MustParseURL("https://login.example.com"),  	}) -	config.Cookies = append(config.Cookies, schema.SessionCookie{ +	config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  		Domain:      "internal.example.com",  		AutheliaURL: MustParseURL("https://login.internal.example.com"),  	})  	ValidateSession(&config, validator)  	assert.False(t, validator.HasWarnings()) -	assert.Len(t, validator.Errors(), 1) +	require.Len(t, validator.Errors(), 1)  	assert.EqualError(t, validator.Errors()[0], "session: domain config #2 (domain 'internal.example.com'): option 'domain' shares the same cookie domain scope as another configured session domain")  } @@ -699,14 +795,18 @@ func TestShouldRaiseErrorWhenDomainIsInvalid(t *testing.T) {  		t.Run(tc.name, func(t *testing.T) {  			validator := schema.NewStructValidator()  			config := newDefaultSessionConfig() -			config.Domain = "" //nolint:staticcheck +			config.Session.Domain = "" //nolint:staticcheck -			config.Cookies = []schema.SessionCookie{ +			config.Session.Cookies = []schema.SessionCookie{  				{  					Domain: tc.have,  				},  			} +			if tc.have != "" { +				config.Session.Cookies[0].AutheliaURL = &url.URL{Scheme: "https", Host: "auth." + tc.have} +			} +  			ValidateSession(&config, validator)  			require.Len(t, validator.Warnings(), len(tc.warnings)) @@ -737,8 +837,8 @@ func TestShouldRaiseErrorWhenPortalURLIsInvalid(t *testing.T) {  		t.Run(tc.name, func(t *testing.T) {  			validator := schema.NewStructValidator()  			config := newDefaultSessionConfig() -			config.Domain = "" //nolint:staticcheck -			config.Cookies = []schema.SessionCookie{ +			config.Session.Domain = "" //nolint:staticcheck +			config.Session.Cookies = []schema.SessionCookie{  				{  					SessionCookieCommon: schema.SessionCookieCommon{  						Name: "authelia_session", @@ -762,7 +862,7 @@ func TestShouldRaiseErrorWhenPortalURLIsInvalid(t *testing.T) {  func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.SameSite = "NOne" +	config.Session.SameSite = "NOne"  	ValidateSession(&config, validator) @@ -776,7 +876,7 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {  func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {  	validator := schema.NewStructValidator() -	var config schema.Session +	var config schema.Configuration  	validOptions := []string{"none", "lax", "strict"} @@ -784,7 +884,7 @@ func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {  		validator.Clear()  		config = newDefaultSessionConfig() -		config.SameSite = opt +		config.Session.SameSite = opt  		ValidateSession(&config, validator) @@ -796,19 +896,19 @@ func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {  func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Inactivity = -1 -	config.Expiration = -1 -	config.RememberMe = schema.RememberMeDisabled +	config.Session.Inactivity = -1 +	config.Session.Expiration = -1 +	config.Session.RememberMe = schema.RememberMeDisabled  	ValidateSession(&config, validator)  	assert.Len(t, validator.Warnings(), 0)  	assert.Len(t, validator.Errors(), 0) -	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) -	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) -	assert.Equal(t, schema.RememberMeDisabled, config.RememberMe) -	assert.True(t, config.DisableRememberMe) +	assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Session.Inactivity) +	assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Session.Expiration) +	assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMe) +	assert.True(t, config.Session.DisableRememberMe)  }  func TestShouldSetDefaultRememberMeDuration(t *testing.T) { @@ -820,34 +920,35 @@ func TestShouldSetDefaultRememberMeDuration(t *testing.T) {  	assert.Len(t, validator.Warnings(), 0)  	assert.Len(t, validator.Errors(), 0) -	assert.Equal(t, config.RememberMe, schema.DefaultSessionConfiguration.RememberMe) +	assert.Equal(t, config.Session.RememberMe, schema.DefaultSessionConfiguration.RememberMe)  }  func TestShouldNotAllowLegacyAndModernCookiesConfig(t *testing.T) {  	validator := schema.NewStructValidator()  	config := newDefaultSessionConfig() -	config.Cookies = append(config.Cookies, schema.SessionCookie{ +	config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{  		SessionCookieCommon: schema.SessionCookieCommon{ -			Name:       config.Name, -			SameSite:   config.SameSite, -			Expiration: config.Expiration, -			Inactivity: config.Inactivity, -			RememberMe: config.RememberMe, +			Name:       config.Session.Name, +			SameSite:   config.Session.SameSite, +			Expiration: config.Session.Expiration, +			Inactivity: config.Session.Inactivity, +			RememberMe: config.Session.RememberMe,  		}, -		Domain: config.Domain, //nolint:staticcheck +		Domain: config.Session.Domain, //nolint:staticcheck  	})  	ValidateSession(&config, validator)  	assert.Len(t, validator.Warnings(), 0) -	require.Len(t, validator.Errors(), 1) +	require.Len(t, validator.Errors(), 2)  	assert.EqualError(t, validator.Errors()[0], "session: option 'domain' and option 'cookies' can't be specified at the same time") +	assert.EqualError(t, validator.Errors()[1], "session: domain config #1 (domain 'example.com'): option 'authelia_url' is required")  }  func MustParseURL(uri string) *url.URL { -	u, err := url.ParseRequestURI(uri) +	u, err := url.Parse(uri)  	if err != nil {  		panic(err) diff --git a/internal/handlers/const_test.go b/internal/handlers/const_test.go index 142775f51..1ce5f8a1e 100644 --- a/internal/handlers/const_test.go +++ b/internal/handlers/const_test.go @@ -1,6 +1,7 @@  package handlers  import ( +	"net/url"  	"time"  	"github.com/valyala/fasthttp" @@ -28,8 +29,19 @@ const (  )  const ( -	testInactivity     = time.Second * 10 -	testRedirectionURL = "http://redirection.local" -	testUsername       = "john" -	exampleDotCom      = "example.com" +	testInactivity           = time.Second * 10 +	testRedirectionURLString = "https://www.example.com" +	testUsername             = "john" +	exampleDotCom            = "example.com" +) + +var ( +	testRedirectionURL = func() *url.URL { +		u, err := url.ParseRequestURI(testRedirectionURLString) +		if err != nil { +			panic(err) +		} + +		return u +	}()  ) diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index ff27fa1b4..ec192fcd6 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -2,6 +2,7 @@ package handlers  import (  	"fmt" +	"net/url"  	"testing"  	"github.com/golang/mock/gomock" @@ -315,7 +316,7 @@ type FirstFactorRedirectionSuite struct {  func (s *FirstFactorRedirectionSuite) SetupTest() {  	s.mock = mocks.NewMockAutheliaCtx(s.T()) -	s.mock.Ctx.Configuration.DefaultRedirectionURL = "https://default.local" +	s.mock.Ctx.Configuration.DefaultRedirectionURL = &url.URL{Scheme: "https", Host: "default.local"}  	s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass  	s.mock.Ctx.Configuration.AccessControl.Rules = []schema.AccessControlRule{  		{ @@ -368,7 +369,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenNoTarget  	FirstFactorPOST(nil)(s.mock.Ctx)  	// Respond with 200. -	s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"}) +	s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})  }  // When: @@ -392,7 +393,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUns  	FirstFactorPOST(nil)(s.mock.Ctx)  	// Respond with 200. -	s.mock.Assert200OK(s.T(), redirectResponse{Redirect: "https://default.local"}) +	s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})  }  // When: diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go index 6602958d5..344cd8eec 100644 --- a/internal/handlers/handler_sign_duo_test.go +++ b/internal/handlers/handler_sign_duo_test.go @@ -532,7 +532,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToDefaultURL() {  	DuoPOST(duoMock)(s.mock.Ctx)  	s.mock.Assert200OK(s.T(), redirectResponse{ -		Redirect: testRedirectionURL, +		Redirect: testRedirectionURLString,  	})  } @@ -578,7 +578,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {  	s.mock.Ctx.Request.SetBody(bodyBytes)  	DuoPOST(duoMock)(s.mock.Ctx) -	s.mock.Assert200OK(s.T(), nil) +	s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})  }  func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() { diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go index f062b6c95..97c466a06 100644 --- a/internal/handlers/handler_sign_totp_test.go +++ b/internal/handlers/handler_sign_totp_test.go @@ -68,7 +68,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {  	TimeBasedOneTimePasswordPOST(s.mock.Ctx)  	s.mock.Assert200OK(s.T(), redirectResponse{ -		Redirect: testRedirectionURL, +		Redirect: testRedirectionURLString,  	})  } @@ -139,7 +139,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {  	s.mock.Ctx.Request.SetBody(bodyBytes)  	TimeBasedOneTimePasswordPOST(s.mock.Ctx) -	s.mock.Assert200OK(s.T(), nil) +	s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})  }  func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() { @@ -260,7 +260,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi  	res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)  	TimeBasedOneTimePasswordPOST(s.mock.Ctx) -	s.mock.Assert200OK(s.T(), nil) +	s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})  	s.NotEqual(  		res[0][1], diff --git a/internal/handlers/handler_state.go b/internal/handlers/handler_state.go index c537d9b65..d77988051 100644 --- a/internal/handlers/handler_state.go +++ b/internal/handlers/handler_state.go @@ -21,9 +21,12 @@ func StateGET(ctx *middlewares.AutheliaCtx) {  	}  	stateResponse := StateResponse{ -		Username:              userSession.Username, -		AuthenticationLevel:   userSession.AuthenticationLevel, -		DefaultRedirectionURL: ctx.Configuration.DefaultRedirectionURL, +		Username:            userSession.Username, +		AuthenticationLevel: userSession.AuthenticationLevel, +	} + +	if uri := ctx.GetDefaultRedirectionURL(); uri != nil { +		stateResponse.DefaultRedirectionURL = uri.String()  	}  	if err = ctx.SetJSONBody(stateResponse); err != nil { diff --git a/internal/handlers/handler_state_test.go b/internal/handlers/handler_state_test.go index 793668374..4897ef75d 100644 --- a/internal/handlers/handler_state_test.go +++ b/internal/handlers/handler_state_test.go @@ -45,7 +45,7 @@ func (s *StateGetSuite) TestShouldReturnUsernameFromSession() {  		Status: "OK",  		Data: StateResponse{  			Username:              "username", -			DefaultRedirectionURL: "", +			DefaultRedirectionURL: "https://www.example.com",  			AuthenticationLevel:   authentication.NotAuthenticated,  		},  	} @@ -77,7 +77,7 @@ func (s *StateGetSuite) TestShouldReturnAuthenticationLevelFromSession() {  		Status: "OK",  		Data: StateResponse{  			Username:              "", -			DefaultRedirectionURL: "", +			DefaultRedirectionURL: "https://www.example.com",  			AuthenticationLevel:   authentication.OneFactor,  		},  	} diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 406b0cbc9..0092fcd56 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -21,8 +21,10 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st  	var err error  	if len(targetURI) == 0 { -		if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { -			if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil { +		defaultRedirectionURL := ctx.GetDefaultRedirectionURL() + +		if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && defaultRedirectionURL != nil { +			if err = ctx.SetJSONBody(redirectResponse{Redirect: defaultRedirectionURL.String()}); err != nil {  				ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)  			}  		} else { @@ -60,8 +62,10 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st  	if !ctx.IsSafeRedirectionTargetURI(targetURL) {  		ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI) -		if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { -			if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil { +		defaultRedirectionURL := ctx.GetDefaultRedirectionURL() + +		if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && defaultRedirectionURL != nil { +			if err = ctx.SetJSONBody(redirectResponse{Redirect: defaultRedirectionURL.String()}); err != nil {  				ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)  			} @@ -85,13 +89,15 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {  	var err error  	if len(targetURI) == 0 { -		if len(ctx.Configuration.DefaultRedirectionURL) == 0 { +		defaultRedirectionURL := ctx.GetDefaultRedirectionURL() + +		if defaultRedirectionURL == nil {  			ctx.ReplyOK()  			return  		} -		if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil { +		if err = ctx.SetJSONBody(redirectResponse{Redirect: defaultRedirectionURL.String()}); err != nil {  			ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)  		} diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go index c985d9333..57b5feb13 100644 --- a/internal/middlewares/authelia_context.go +++ b/internal/middlewares/authelia_context.go @@ -396,6 +396,15 @@ func (ctx *AutheliaCtx) DestroySession() error {  	return provider.DestroySession(ctx.RequestCtx)  } +// GetDefaultRedirectionURL retrieves the default redirection URL for the request. +func (ctx *AutheliaCtx) GetDefaultRedirectionURL() *url.URL { +	if provider, err := ctx.GetSessionProvider(); err == nil { +		return provider.Config.DefaultRedirectionURL +	} + +	return ctx.Configuration.DefaultRedirectionURL +} +  // ReplyOK is a helper method to reply ok.  func (ctx *AutheliaCtx) ReplyOK() {  	ctx.SetContentTypeApplicationJSON() diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go index 4092b24cd..3292f181d 100644 --- a/internal/middlewares/authelia_context_test.go +++ b/internal/middlewares/authelia_context_test.go @@ -546,3 +546,23 @@ func TestAutheliaCtx_GetTargetURICookieDomain(t *testing.T) {  		})  	}  } + +func TestAutheliaCtx_GetDefaultRedirectionURL(t *testing.T) { +	mock := mocks.NewMockAutheliaCtx(t) +	defer mock.Close() + +	mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.example4.com/consent") + +	assert.Equal(t, &url.URL{Scheme: "https", Host: "fallback.example.com"}, mock.Ctx.GetDefaultRedirectionURL()) + +	mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.example.com/consent") + +	assert.Equal(t, &url.URL{Scheme: "https", Host: "www.example.com"}, mock.Ctx.GetDefaultRedirectionURL()) + +	mock2 := mocks.NewMockAutheliaCtx(t) +	defer mock2.Close() + +	mock2.Ctx.Request.Header.Set("X-Original-URL", "https://auth.example2.com/consent") + +	assert.Equal(t, &url.URL{Scheme: "https", Host: "www.example2.com"}, mock2.Ctx.GetDefaultRedirectionURL()) +} diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go index 3de8f2f52..1ededfa1d 100644 --- a/internal/mocks/authelia_ctx.go +++ b/internal/mocks/authelia_ctx.go @@ -3,6 +3,7 @@ package mocks  import (  	"encoding/json"  	"fmt" +	"net/url"  	"testing"  	"time" @@ -50,7 +51,10 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {  	datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")  	mockAuthelia.Clock.Set(datetime) -	config := schema.Configuration{} +	config := schema.Configuration{ +		DefaultRedirectionURL: &url.URL{Scheme: "https", Host: "fallback.example.com"}, +	} +  	config.Session.Cookies = []schema.SessionCookie{  		{  			SessionCookieCommon: schema.SessionCookieCommon{ @@ -58,7 +62,8 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {  				RememberMe: schema.DefaultSessionConfiguration.RememberMe,  				Expiration: schema.DefaultSessionConfiguration.Expiration,  			}, -			Domain: "example.com", +			Domain:                "example.com", +			DefaultRedirectionURL: &url.URL{Scheme: "https", Host: "www.example.com"},  		},  		{  			SessionCookieCommon: schema.SessionCookieCommon{ @@ -66,7 +71,8 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {  				RememberMe: schema.DefaultSessionConfiguration.RememberMe,  				Expiration: schema.DefaultSessionConfiguration.Expiration,  			}, -			Domain: "example2.com", +			Domain:                "example2.com", +			DefaultRedirectionURL: &url.URL{Scheme: "https", Host: "www.example2.com"},  		},  	} diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml index cf9c504ad..0ad97c5b3 100644 --- a/internal/suites/ActiveDirectory/configuration.yml +++ b/internal/suites/ActiveDirectory/configuration.yml @@ -5,7 +5,6 @@  theme: grey  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -37,6 +36,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  storage:    encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml index 15dacd9f8..51d3c62ec 100644 --- a/internal/suites/Docker/configuration.yml +++ b/internal/suites/Docker/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -27,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  storage:    encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml index 3235ce73c..3945772e9 100644 --- a/internal/suites/DuoPush/configuration.yml +++ b/internal/suites/DuoPush/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -27,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  # Configuration of the storage backend used to store data and secrets. i.e. totp data  storage: diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index ceb0115d3..2784c618b 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -5,7 +5,6 @@  theme: dark  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -44,6 +43,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  storage:    encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/MariaDB/configuration.yml b/internal/suites/MariaDB/configuration.yml index 858040965..27c05eda4 100644 --- a/internal/suites/MariaDB/configuration.yml +++ b/internal/suites/MariaDB/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -27,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  # Configuration of the storage backend used to store data and secrets. i.e. totp data  storage: diff --git a/internal/suites/MySQL/configuration.yml b/internal/suites/MySQL/configuration.yml index 062677e9d..a5c12f551 100644 --- a/internal/suites/MySQL/configuration.yml +++ b/internal/suites/MySQL/configuration.yml @@ -12,8 +12,6 @@ server:  log:    level: debug -default_redirection_url: https://home.example.com:8080/ -  jwt_secret: very_important_secret  authentication_backend: @@ -28,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  # Configuration of the storage backend used to store data and secrets. i.e. totp data  storage: diff --git a/internal/suites/OneFactorOnly/configuration.yml b/internal/suites/OneFactorOnly/configuration.yml index 21f6cc5f0..b7a5ad5d4 100644 --- a/internal/suites/OneFactorOnly/configuration.yml +++ b/internal/suites/OneFactorOnly/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: unsecure_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -27,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  storage:    encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml index 05d952bbb..03e296f25 100644 --- a/internal/suites/Postgres/configuration.yml +++ b/internal/suites/Postgres/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: very_important_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -27,6 +26,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/  # Configuration of the storage backend used to store data and secrets. i.e. totp data  storage: diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml index e330dca6c..4aacc72e6 100644 --- a/internal/suites/ShortTimeouts/configuration.yml +++ b/internal/suites/ShortTimeouts/configuration.yml @@ -4,7 +4,6 @@  ###############################################################  jwt_secret: unsecure_secret -default_redirection_url: https://home.example.com:8080/  server:    address: 'tcp://:9091' @@ -25,6 +24,7 @@ session:      - name: 'authelia_sessin'        domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080/        inactivity: 5        expiration: 8        remember_me: 1y diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml index d02eba4c5..2b822978f 100644 --- a/internal/suites/example/kube/authelia/configs/configuration.yml +++ b/internal/suites/example/kube/authelia/configs/configuration.yml @@ -3,8 +3,6 @@  #                   Authelia configuration                    #  ############################################################### -default_redirection_url: https://home.example.com:8080 -  server:    address: 'tcp://:443'    tls: @@ -54,6 +52,7 @@ session:    cookies:      - domain: 'example.com'        authelia_url: 'https://login.example.com:8080' +      default_redirection_url: https://home.example.com:8080    redis:      host: redis-service diff --git a/internal/utils/url.go b/internal/utils/url.go index 037591527..04e737400 100644 --- a/internal/utils/url.go +++ b/internal/utils/url.go @@ -69,3 +69,38 @@ func HasDomainSuffix(domain, domainSuffix string) bool {  	return false  } + +// EqualURLs returns true if the two *url.URL values are effectively equal taking into consideration web normalization. +func EqualURLs(first, second *url.URL) bool { +	if first == nil && second == nil { +		return true +	} else if first == nil || second == nil { +		return false +	} + +	if !strings.EqualFold(first.Scheme, second.Scheme) { +		return false +	} + +	if !strings.EqualFold(first.Host, second.Host) { +		return false +	} + +	if first.Path != second.Path { +		return false +	} + +	if first.RawQuery != second.RawQuery { +		return false +	} + +	if first.Fragment != second.Fragment { +		return false +	} + +	if first.RawFragment != second.RawFragment { +		return false +	} + +	return true +} diff --git a/internal/utils/url_test.go b/internal/utils/url_test.go index dc8ab1508..f6ddd6a2d 100644 --- a/internal/utils/url_test.go +++ b/internal/utils/url_test.go @@ -64,3 +64,26 @@ func TestHasDomainSuffix(t *testing.T) {  	assert.False(t, HasDomainSuffix("abc", ""))  	assert.False(t, HasDomainSuffix("", ""))  } + +func TestEqualURLs(t *testing.T) { +	assert.False(t, EqualURLs(MustParseURL(url.Parse("https://google.com/abc#frag")), MustParseURL(url.Parse("https://google.com/abc")))) +	assert.False(t, EqualURLs(&url.URL{Scheme: "https", Host: "example.com", RawFragment: "example"}, &url.URL{Scheme: "https", Host: "example.com"})) + +	assert.True(t, EqualURLs(MustParseURL(url.Parse("https://google.com")), MustParseURL(url.Parse("https://google.com")))) +	assert.True(t, EqualURLs(MustParseURL(url.Parse("https://google.com")), MustParseURL(url.Parse("https://Google.com")))) +	assert.True(t, EqualURLs(MustParseURL(url.Parse("https://google.com/abc")), MustParseURL(url.Parse("https://Google.com/abc")))) +	assert.False(t, EqualURLs(MustParseURL(url.Parse("https://google.com/abc")), MustParseURL(url.Parse("https://Google.com/ABC")))) +	assert.False(t, EqualURLs(MustParseURL(url.Parse("https://google.com/abc?abc=1")), MustParseURL(url.Parse("https://Google.com/abc")))) +	assert.False(t, EqualURLs(MustParseURL(url.Parse("https://google2.com/abc")), MustParseURL(url.Parse("https://Google.com/abc")))) +	assert.False(t, EqualURLs(MustParseURL(url.Parse("http://google.com/abc")), MustParseURL(url.Parse("https://Google.com/abc")))) +	assert.True(t, EqualURLs(nil, nil)) +	assert.False(t, EqualURLs(nil, MustParseURL(url.Parse("http://google.com/abc")))) +} + +func MustParseURL(uri *url.URL, err error) *url.URL { +	if err != nil { +		panic(err) +	} + +	return uri +}  | 
