authelia/internal/configuration/validator/session.go

260 lines
8.6 KiB
Go

package validator
import (
"fmt"
"path"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateSession validates and update session configuration.
func ValidateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Name == "" {
config.Name = schema.DefaultSessionConfiguration.Name
}
if config.Redis != nil {
if config.Redis.HighAvailability != nil {
validateRedisSentinel(config, validator)
} else {
validateRedis(config, validator)
}
}
validateSession(config, validator)
}
func validateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Expiration <= 0 {
config.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour.
}
if config.Inactivity <= 0 {
config.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.
}
if config.SameSite == "" {
config.SameSite = schema.DefaultSessionConfiguration.SameSite
} else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
validator.Push(fmt.Errorf(errFmtSessionSameSite, strJoinOr(validSessionSameSiteValues), config.SameSite))
}
cookies := len(config.Cookies)
switch {
case cookies == 0 && config.Domain != "":
// Add legacy configuration to the domains list.
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
Name: config.Name,
Domain: config.Domain,
SameSite: config.SameSite,
Expiration: config.Expiration,
Inactivity: config.Inactivity,
RememberMe: config.RememberMe,
DisableRememberMe: config.DisableRememberMe,
},
})
case cookies != 0 && config.Domain != "":
validator.Push(fmt.Errorf(errFmtSessionLegacyAndWarning))
}
validateSessionCookieDomains(config, validator)
}
func validateSessionCookieDomains(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if len(config.Cookies) == 0 {
validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "cookies"))
}
domains := make([]string, 0)
for i, d := range config.Cookies {
validateSessionDomainName(i, config, validator)
validateSessionUniqueCookieDomain(i, config, domains, validator)
validateSessionCookieName(i, config)
validateSessionSafeRedirection(i, config, validator)
validateSessionExpiration(i, config)
validateSessionRememberMe(i, config)
validateSessionSameSite(i, config, validator)
domains = append(domains, d.Domain)
}
}
// validateSessionDomainName returns error if the domain name is invalid.
func validateSessionDomainName(i int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
var d = config.Cookies[i]
switch {
case d.Domain == "":
validator.Push(fmt.Errorf(errFmtSessionDomainRequired, sessionDomainDescriptor(i, d)))
return
case strings.HasPrefix(d.Domain, "*."):
validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain))
return
case strings.HasPrefix(d.Domain, "."):
validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d)))
case !strings.Contains(d.Domain, "."):
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainNoDots, sessionDomainDescriptor(i, d)))
return
case !reDomainCharacters.MatchString(d.Domain):
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, sessionDomainDescriptor(i, d)))
return
}
if isCookieDomainAPublicSuffix(d.Domain) {
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainPublic, sessionDomainDescriptor(i, d)))
}
}
func validateSessionCookieName(i int, config *schema.SessionConfiguration) {
if config.Cookies[i].Name == "" {
config.Cookies[i].Name = config.Name
}
}
func validateSessionExpiration(i int, config *schema.SessionConfiguration) {
if config.Cookies[i].Expiration <= 0 {
config.Cookies[i].Expiration = config.Expiration
}
if config.Cookies[i].Inactivity <= 0 {
config.Cookies[i].Inactivity = config.Inactivity
}
}
// validateSessionUniqueCookieDomain Check the current domains do not share a root domain with previous domains.
func validateSessionUniqueCookieDomain(i int, config *schema.SessionConfiguration, domains []string, validator *schema.StructValidator) {
var d = config.Cookies[i]
if utils.IsStringInSliceF(d.Domain, domains, utils.HasDomainSuffix) {
if utils.IsStringInSlice(d.Domain, domains) {
validator.Push(fmt.Errorf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(i, d)))
} else {
validator.Push(fmt.Errorf(errFmtSessionDomainDuplicateCookieScope, sessionDomainDescriptor(i, d)))
}
}
}
// validateSessionSafeRedirection validates that AutheliaURL is safe for redirection.
func validateSessionSafeRedirection(index int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
var d = config.Cookies[index]
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))
}
}
}
func validateSessionRememberMe(i int, config *schema.SessionConfiguration) {
if config.Cookies[i].RememberMe <= 0 && config.Cookies[i].RememberMe != schema.RememberMeDisabled {
config.Cookies[i].RememberMe = config.RememberMe
}
if config.Cookies[i].RememberMe == schema.RememberMeDisabled {
config.Cookies[i].DisableRememberMe = true
}
}
func validateSessionSameSite(i int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Cookies[i].SameSite == "" {
if utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
config.Cookies[i].SameSite = config.SameSite
} else {
config.Cookies[i].SameSite = schema.DefaultSessionConfiguration.SameSite
}
} else if !utils.IsStringInSlice(config.Cookies[i].SameSite, validSessionSameSiteValues) {
validator.Push(fmt.Errorf(errFmtSessionDomainSameSite, sessionDomainDescriptor(i, config.Cookies[i]), strJoinOr(validSessionSameSiteValues), config.Cookies[i].SameSite))
}
}
func sessionDomainDescriptor(position int, domain schema.SessionCookieConfiguration) string {
return fmt.Sprintf("#%d (domain '%s')", position+1, domain.Domain)
}
func validateRedisCommon(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Secret == "" {
validator.Push(fmt.Errorf(errFmtSessionSecretRequired, "redis"))
}
if config.Redis.TLS != nil {
configDefaultTLS := &schema.TLSConfig{
ServerName: config.Redis.Host,
MinimumVersion: schema.DefaultRedisConfiguration.TLS.MinimumVersion,
MaximumVersion: schema.DefaultRedisConfiguration.TLS.MaximumVersion,
}
if err := ValidateTLSConfig(config.Redis.TLS, configDefaultTLS); err != nil {
validator.Push(fmt.Errorf(errFmtSessionRedisTLSConfigInvalid, err))
}
}
}
func validateRedis(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Redis.Host == "" {
validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired))
}
validateRedisCommon(config, validator)
if !path.IsAbs(config.Redis.Host) && (config.Redis.Port < 1 || config.Redis.Port > 65535) {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
}
if config.Redis.MaximumActiveConnections <= 0 {
config.Redis.MaximumActiveConnections = 8
}
}
func validateRedisSentinel(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if config.Redis.HighAvailability.SentinelName == "" {
validator.Push(fmt.Errorf(errFmtSessionRedisSentinelMissingName))
}
if config.Redis.Port == 0 {
config.Redis.Port = 26379
} else if config.Redis.Port < 0 || config.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
}
if config.Redis.Host == "" && len(config.Redis.HighAvailability.Nodes) == 0 {
validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired))
}
validateRedisCommon(config, validator)
hostMissing := false
for i, node := range config.Redis.HighAvailability.Nodes {
if node.Host == "" {
hostMissing = true
}
if node.Port == 0 {
config.Redis.HighAvailability.Nodes[i].Port = 26379
}
}
if hostMissing {
validator.Push(fmt.Errorf(errFmtSessionRedisSentinelNodeHostMissing))
}
}