diff --git a/.editorconfig b/.editorconfig index cc3c38a0c..11e3deb80 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ trim_trailing_whitespace = true end_of_line = lf insert_final_newline = true -[*.{sh,yml,yaml}] +[*.{html,sh,yml,yaml}] indent_style = space indent_size = 2 diff --git a/cmd/authelia-gen/types.go b/cmd/authelia-gen/types.go index 99e13fd45..479f0410d 100644 --- a/cmd/authelia-gen/types.go +++ b/cmd/authelia-gen/types.go @@ -79,7 +79,7 @@ const ( type labelPriority int -//nolint:deadcode // Kept for future use. +//nolint:deadcode,varcheck // Kept for future use. const ( labelPriorityCritical labelPriority = iota labelPriorityHigh @@ -122,7 +122,7 @@ func (s labelStatus) String() string { type labelType int -//nolint:deadcode // Kept for future use. +//nolint:deadcode,varcheck // Kept for future use. const ( labelTypeFeature labelType = iota labelTypeBugUnconfirmed diff --git a/cmd/authelia-scripts/cmd/bootstrap.go b/cmd/authelia-scripts/cmd/bootstrap.go index 0e43531d6..82cd8a0c0 100644 --- a/cmd/authelia-scripts/cmd/bootstrap.go +++ b/cmd/authelia-scripts/cmd/bootstrap.go @@ -114,6 +114,30 @@ var hostEntries = []HostEntry{ {Domain: "redis-sentinel-0.example.com", IP: "192.168.240.120"}, {Domain: "redis-sentinel-1.example.com", IP: "192.168.240.121"}, {Domain: "redis-sentinel-2.example.com", IP: "192.168.240.122"}, + + // For multi cookie domain tests. + {Domain: "login.example2.com", IP: "192.168.240.100"}, + {Domain: "admin.example2.com", IP: "192.168.240.100"}, + {Domain: "singlefactor.example2.com", IP: "192.168.240.100"}, + {Domain: "dev.example2.com", IP: "192.168.240.100"}, + {Domain: "home.example2.com", IP: "192.168.240.100"}, + {Domain: "mx1.mail.example2.com", IP: "192.168.240.100"}, + {Domain: "mx2.mail.example2.com", IP: "192.168.240.100"}, + {Domain: "public.example2.com", IP: "192.168.240.100"}, + {Domain: "secure.example2.com", IP: "192.168.240.100"}, + {Domain: "mail.example2.com", IP: "192.168.240.100"}, + {Domain: "duo.example2.com", IP: "192.168.240.100"}, + {Domain: "login.example3.com", IP: "192.168.240.100"}, + {Domain: "admin.example3.com", IP: "192.168.240.100"}, + {Domain: "singlefactor.example3.com", IP: "192.168.240.100"}, + {Domain: "dev.example3.com", IP: "192.168.240.100"}, + {Domain: "home.example3.com", IP: "192.168.240.100"}, + {Domain: "mx1.mail.example3.com", IP: "192.168.240.100"}, + {Domain: "mx2.mail.example3.com", IP: "192.168.240.100"}, + {Domain: "public.example3.com", IP: "192.168.240.100"}, + {Domain: "secure.example3.com", IP: "192.168.240.100"}, + {Domain: "mail.example3.com", IP: "192.168.240.100"}, + {Domain: "duo.example3.com", IP: "192.168.240.100"}, } func runCommand(cmd string, args ...string) { diff --git a/config.template.yml b/config.template.yml index 6dc5a39eb..0d966b038 100644 --- a/config.template.yml +++ b/config.template.yml @@ -662,38 +662,76 @@ access_control: ## The session cookies identify the user once logged in. ## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined. session: - ## The name of the session cookie. - name: authelia_session - - ## The domain to protect. - ## Note: the authenticator must also be in that domain. - ## If empty, the cookie is restricted to the subdomain of the issuer. - domain: 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 - ## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel. ## Secret can also be set using a secret: https://www.authelia.com/c/secrets - secret: insecure_session_secret + secret: 'insecure_session_secret' - ## The value for expiration, inactivity, and remember_me_duration are in seconds or the duration notation format. + ## Cookies configures the list of allowed cookie domains for sessions to be created on. + ## Undefined values will default to the values below. + # cookies: + # - + ## The name of the session cookie. + # name: 'authelia_session' + + ## The domain to protect. + ## 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. + ## Rules: + ## - MUST use the secure scheme 'https://' + ## - The above domain 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' + + ## 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' + + ## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format. + ## See: https://www.authelia.com/c/common#duration-notation-format + ## All three of these values affect the cookie/session validity period. Longer periods are considered less secure + ## because a stolen cookie will last longer giving attackers more time to spy or attack. + + ## The inactivity time before the session is reset. If expiration is set to 1h, and this is set to 5m, if the user + ## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last + ## time Authelia detected user activity. + # inactivity: '5m' + + ## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the + ## user. + # expiration: '1h' + + ## The time before the cookie expires and the session is destroyed if remember me IS selected by the user. Setting + ## this value to -1 disables remember me for this session cookie domain. + # remember_me: '1M' + + ## Cookie Session Domain default 'name' value. The name of the session cookie. + name: 'authelia_session' + + ## Cookie Session Domain default 'same_site' value. 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' + + ## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format. ## See: https://www.authelia.com/c/common#duration-notation-format ## All three of these values affect the cookie/session validity period. Longer periods are considered less secure ## because a stolen cookie will last longer giving attackers more time to spy or attack. - ## The time before the cookie expires and the session is destroyed if remember me IS NOT selected. - expiration: 1h + ## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is + ## set to 1h, and this is set to 5m, if the user does not select the remember me option their session will get + ## destroyed after 1h, or after 5m since the last time Authelia detected user activity. + inactivity: '5m' - ## The inactivity time before the session is reset. If expiration is set to 1h, and this is set to 5m, if the user - ## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last time - ## Authelia detected user activity. - inactivity: 5m + ## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is + ## destroyed if remember me IS NOT selected by the user. + expiration: '1h' - ## The time before the cookie expires and the session is destroyed if remember me IS selected. - ## Value of -1 disables remember me. - remember_me_duration: 1M + ## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed + ## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie + ## domains which do not have a specific 'remember_me' value. + remember_me: '1M' ## ## Redis Provider diff --git a/docs/content/en/configuration/prologue/common.md b/docs/content/en/configuration/prologue/common.md index 1f4be33a1..2a8c8b381 100644 --- a/docs/content/en/configuration/prologue/common.md +++ b/docs/content/en/configuration/prologue/common.md @@ -22,7 +22,7 @@ describes the implementation of this. You can use this implementation in various * session: * expiration * inactivity - * remember_me_duration + * remember_me * regulation: * ban_time * find_time diff --git a/docs/content/en/configuration/session/introduction.md b/docs/content/en/configuration/session/introduction.md index 24e54f1aa..67d9290cf 100644 --- a/docs/content/en/configuration/session/introduction.md +++ b/docs/content/en/configuration/session/introduction.md @@ -25,13 +25,21 @@ authenticated user and can then order the reverse proxy to let the request pass ```yaml session: + secret: insecure_session_secret + name: authelia_session - domain: example.com same_site: lax - secret: unsecure_session_secret - expiration: 1h inactivity: 5m - remember_me_duration: 1M + expiration: 1h + remember_me: 1M + + cookies: + - name: authelia_session + domain: example.com + same_site: lax + inactivity: 5m + expiration: 1h + remember_me: 1d ``` ## Providers @@ -50,34 +58,6 @@ providers are recommended. ## Options -### name - -{{< confkey type="string" default="authelia_session" required="no" >}} - -The name of the session cookie. By default this is set to authelia_session. It's mostly useful to change this if you are -doing development or running multiple instances of Authelia. - -### domain - -{{< confkey type="string" required="yes" >}} - -The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root -of the domain. For example if listening on auth.example.com the cookie should be auth.example.com or example.com. - -### same_site - -{{< confkey type="string" default="lax" required="no" >}} - -Sets the cookies SameSite value. Prior to offering the configuration choice this defaulted to None. The new default is -Lax. This option is defined in lower-case. So for example if you want to set it to `Strict`, the value in configuration -needs to be `strict`. - -You can read about the SameSite cookie in detail on the -[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). In short setting SameSite to Lax -is generally the most desirable option for Authelia. None is not recommended unless you absolutely know what you're -doing and trust all the protected apps. Strict is not going to work in many use cases and we have not tested it in this -state but it's available as an option anyway. - ### secret {{< confkey type="string" required="yes" >}} @@ -91,15 +71,29 @@ It's __strongly recommended__ this is a [Random Alphanumeric String](../../reference/guides/generating-secure-values.md#generating-a-random-alphanumeric-string) with 64 or more characters. -### expiration +### domain -{{< confkey type="duration" default="1h" required="no" >}} +{{< confkey type="string" required="no" >}} -*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see -the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* +_**Deprecation Notice:** This option is deprecated. See the [cookies](#cookies) section instead._ -The period of time before the cookie expires and the session is destroyed. This is overriden by -[remember_me_duration](#remembermeduration) when the remember me box is checked. +The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root +of the domain. For example if listening on auth.example.com the cookie should be auth.example.com or example.com. + +This value automatically maps to a single cookies configuration using the default values. It cannot be assigned at the +same time as a `cookies` configuration. + +### name + +{{< confkey type="string" default="authelia_session" required="no" >}} + +The default `name` value for all [cookies](#cookies) configurations. + +### same_site + +{{< confkey type="string" default="lax" required="no" >}} + +The default `same_site` value for all `cookies` configurations. ### inactivity @@ -108,18 +102,119 @@ The period of time before the cookie expires and the session is destroyed. This *__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* -The period of time the user can be inactive for until the session is destroyed. Useful if you want long session timers -but don't want unused devices to be vulnerable. +The default `inactivity` value for all [cookies](#cookies) configurations. -### remember_me_duration +### expiration + +{{< confkey type="duration" default="1h" required="no" >}} + +*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see +the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* + +The default `expiration` value for all [cookies](#cookies) configurations. + +### remember_me {{< confkey type="duration" default="1M" required="no" >}} *__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* +The default `remember_me` value for all [cookies](#cookies) configurations. + +### cookies + +The list of specific cookie domains that Authelia is configured to handle. Domains not properly configured will +automatically be denied by Authelia. The list allows administrators to define multiple session cookie domain +configurations with individual settings. + +#### name + +{{< confkey type="string" required="no" >}} + +*__Default Value:__ This option takes its default value from the [name](#name) setting above.* + +The name of the session cookie. By default this is set to the `name` value in the main session configuration section. + +#### domain + +{{< confkey type="string" required="yes" >}} + +The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root +of the domain, and consequently if the [authelia_url](#authelia_url) is configured must be able to read and write cookies +for the domain. For example if listening on `auth.example.com` the cookie should be either `auth.example.com` or +`example.com`. + +Please note most good DynamicDNS solutions fall into a specially protected group of domains and browsers do not allow +you to write cookies for the root domain. i.e. if you have been assigned `john.duckdns.org` you can't use `duckdns.org` +for the domain value as browsers will not allow `john.duckdns.org` to read or write cookies for `duckdns.org`. + +Consequently, if you have `john.duckdns.org` and `mary.duckdns.org` you cannot share cookies between these domains. + +#### authelia_url + +{{< confkey type="string" required="no" >}} + +*__Note:__ The AuthRequest implementation does not support redirection control on the authorization server. This means +that the `authelia_url` option is ineffectual for both NGINX and HAProxy, or any other proxy which uses the AuthRequest +implementation.* + +This is a completely optional URL which is the root URL of your Authelia installation for this cookie domain which can +be used to generate the appropriate redirection for proxies which support this. + +If this option is absent you must use the appropriate query parameter or header for your relevant proxy. + +#### same_site + +{{< confkey type="string" required="no" >}} + +*__Default Value:__ This option takes its default value from the [same_site](#samesite) setting above.* + +Sets the cookies SameSite value. Prior to offering the configuration choice this defaulted to None. The new default is +Lax. This option is defined in lower-case. So for example if you want to set it to `Strict`, the value in configuration +needs to be `strict`. + +You can read about the SameSite cookie in detail on the +[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). In short setting SameSite to Lax +is generally the most desirable option for Authelia. None is not recommended unless you absolutely know what you're +doing and trust all the protected apps. Strict is not going to work in many use cases and we have not tested it in this +state but it's available as an option anyway. + +#### inactivity + +{{< confkey type="duration" required="no" >}} + +*__Default Value:__ This option takes its default value from the [inactivity](#inactivity) setting above.* + +*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see +the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* + +The period of time the user can be inactive for until the session is destroyed. Useful if you want long session timers +but don't want unused devices to be vulnerable. + +#### expiration + +{{< confkey type="duration" required="no" >}} + +*__Default Value:__ This option takes its default value from the [expiration](#expiration) setting above.* + +*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see +the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* + +The period of time before the cookie expires and the session is destroyed. This is overriden by +[remember_me](#rememberme) when the remember me box is checked. + +#### remember_me + +{{< confkey type="duration" required="no" >}} + +*__Default Value:__ This option takes its default value from the [remember_me](#rememberme) setting above.* + +*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see +the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* + The period of time before the cookie expires and the session is destroyed when the remember me box is checked. Setting -this to `-1` disables this feature entirely. +this to `-1` disables this feature entirely for this session cookie domain. ## Security diff --git a/docs/content/en/contributing/guidelines/commit-message.md b/docs/content/en/contributing/guidelines/commit-message.md index 8683eb194..5d67c65e6 100644 --- a/docs/content/en/contributing/guidelines/commit-message.md +++ b/docs/content/en/contributing/guidelines/commit-message.md @@ -51,8 +51,8 @@ for, and the structure it must have. │ └─⫸ Commit Scope: api|autheliabot|authentication|authorization|buildkite|bundler|cmd| │ codecov|commands|configuration|deps|docker|duo|go|golangci-lint| │ handlers|logging|metrics|middlewares|mocks|model|notification|npm|ntp| - │ oidc|regulation|renovate|reviewdog|server|session|storage|suites| - │ templates|totp|utils|web + │ oidc|random|regulation|renovate|reviewdog|server|session|storage| + │ suites|templates|totp|utils|web │ └─⫸ Commit Type: build|ci|docs|feat|fix|i18n|perf|refactor|release|revert|test ``` @@ -93,6 +93,7 @@ commit messages). * notification * ntp * oidc +* random * regulation * server * session diff --git a/docs/content/en/overview/security/measures.md b/docs/content/en/overview/security/measures.md index d94b5c7fe..b050512fc 100644 --- a/docs/content/en/overview/security/measures.md +++ b/docs/content/en/overview/security/measures.md @@ -256,7 +256,7 @@ database. The value of this option should be long and as random as possible. See [documentation](../../configuration/session/introduction.md#secret) for this option. The validity period of session is highly configurable. For example in a highly security conscious domain you could -set the session [remember_me_duration](../../configuration/session/introduction.md#remembermeduration) to 0 to disable this +set the session [remember_me](../../configuration/session/introduction.md#rememberme) to 0 to disable this feature, and set the [expiration](../../configuration/session/introduction.md#expiration) to 2 hours and the [inactivity](../../configuration/session/introduction.md#inactivity) of 10 minutes. Configuring the session security in this manner would mean if the cookie age was more than 2 hours or if the user was inactive for more than 10 minutes the diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json index 20377f12a..1d0937984 100644 --- a/docs/data/configkeys.json +++ b/docs/data/configkeys.json @@ -1 +1 @@ -[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me_duration","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME_DURATION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENABLE_PPROF"},{"path":"server.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENABLE_EXPVARS"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"}] \ No newline at end of file +[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.cookies","secret":false,"env":"AUTHELIA_SESSION_COOKIES"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENABLE_PPROF"},{"path":"server.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENABLE_EXPVARS"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"}] \ No newline at end of file diff --git a/examples/compose/lite/authelia/configuration.yml b/examples/compose/lite/authelia/configuration.yml index 0804550ae..d9841d310 100644 --- a/examples/compose/lite/authelia/configuration.yml +++ b/examples/compose/lite/authelia/configuration.yml @@ -39,12 +39,14 @@ access_control: policy: two_factor session: - name: authelia_session # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE secret: unsecure_session_secret - expiration: 3600 # 1 hour - inactivity: 300 # 5 minutes - domain: example.com # Should match whatever your root protected domain is + + cookies: + - name: authelia_session + domain: example.com # Should match whatever your root protected domain is + expiration: 3600 # 1 hour + inactivity: 300 # 5 minutes redis: host: redis diff --git a/examples/compose/local/authelia/configuration.yml b/examples/compose/local/authelia/configuration.yml index 84b8039c3..42b770e6c 100644 --- a/examples/compose/local/authelia/configuration.yml +++ b/examples/compose/local/authelia/configuration.yml @@ -31,11 +31,13 @@ access_control: policy: two_factor session: - name: authelia_session secret: unsecure_session_secret - expiration: 3600 # 1 hour - inactivity: 300 # 5 minutes - domain: example.com # Should match whatever your root protected domain is + + cookies: + - name: authelia_session + domain: example.com # Should match whatever your root protected domain is + expiration: 3600 # 1 hour + inactivity: 300 # 5 minutes regulation: max_retries: 3 diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 6dc5a39eb..0d966b038 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -662,38 +662,76 @@ access_control: ## The session cookies identify the user once logged in. ## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined. session: - ## The name of the session cookie. - name: authelia_session - - ## The domain to protect. - ## Note: the authenticator must also be in that domain. - ## If empty, the cookie is restricted to the subdomain of the issuer. - domain: 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 - ## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel. ## Secret can also be set using a secret: https://www.authelia.com/c/secrets - secret: insecure_session_secret + secret: 'insecure_session_secret' - ## The value for expiration, inactivity, and remember_me_duration are in seconds or the duration notation format. + ## Cookies configures the list of allowed cookie domains for sessions to be created on. + ## Undefined values will default to the values below. + # cookies: + # - + ## The name of the session cookie. + # name: 'authelia_session' + + ## The domain to protect. + ## 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. + ## Rules: + ## - MUST use the secure scheme 'https://' + ## - The above domain 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' + + ## 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' + + ## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format. + ## See: https://www.authelia.com/c/common#duration-notation-format + ## All three of these values affect the cookie/session validity period. Longer periods are considered less secure + ## because a stolen cookie will last longer giving attackers more time to spy or attack. + + ## The inactivity time before the session is reset. If expiration is set to 1h, and this is set to 5m, if the user + ## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last + ## time Authelia detected user activity. + # inactivity: '5m' + + ## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the + ## user. + # expiration: '1h' + + ## The time before the cookie expires and the session is destroyed if remember me IS selected by the user. Setting + ## this value to -1 disables remember me for this session cookie domain. + # remember_me: '1M' + + ## Cookie Session Domain default 'name' value. The name of the session cookie. + name: 'authelia_session' + + ## Cookie Session Domain default 'same_site' value. 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' + + ## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format. ## See: https://www.authelia.com/c/common#duration-notation-format ## All three of these values affect the cookie/session validity period. Longer periods are considered less secure ## because a stolen cookie will last longer giving attackers more time to spy or attack. - ## The time before the cookie expires and the session is destroyed if remember me IS NOT selected. - expiration: 1h + ## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is + ## set to 1h, and this is set to 5m, if the user does not select the remember me option their session will get + ## destroyed after 1h, or after 5m since the last time Authelia detected user activity. + inactivity: '5m' - ## The inactivity time before the session is reset. If expiration is set to 1h, and this is set to 5m, if the user - ## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last time - ## Authelia detected user activity. - inactivity: 5m + ## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is + ## destroyed if remember me IS NOT selected by the user. + expiration: '1h' - ## The time before the cookie expires and the session is destroyed if remember me IS selected. - ## Value of -1 disables remember me. - remember_me_duration: 1M + ## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed + ## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie + ## domains which do not have a specific 'remember_me' value. + remember_me: '1M' ## ## Redis Provider diff --git a/internal/configuration/deprecation.go b/internal/configuration/deprecation.go index fbe881ef1..e37750996 100644 --- a/internal/configuration/deprecation.go +++ b/internal/configuration/deprecation.go @@ -134,4 +134,11 @@ var deprecations = map[string]Deprecation{ AutoMap: true, MapFunc: nil, }, + "session.remember_me_duration": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "session.remember_me_duration", + NewKey: "session.remember_me", + AutoMap: true, + MapFunc: nil, + }, } diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 04ae71890..975fccdd4 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -323,7 +323,7 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) { assert.Equal(t, "Admin", config.Notifier.SMTP.Sender.Name) assert.Equal(t, "admin@example.com", config.Notifier.SMTP.Sender.Address) - assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMeDuration) + assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMe) } func TestShouldParseRegex(t *testing.T) { diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index 07967dc88..d1d616b44 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -105,13 +105,23 @@ var Keys = []string{ "authentication_backend.ldap.permit_feature_detection_failure", "authentication_backend.ldap.user", "authentication_backend.ldap.password", + "session.secret", "session.name", "session.domain", "session.same_site", - "session.secret", "session.expiration", "session.inactivity", - "session.remember_me_duration", + "session.remember_me", + "session", + "session.cookies", + "session.cookies[].name", + "session.cookies[].domain", + "session.cookies[].same_site", + "session.cookies[].expiration", + "session.cookies[].inactivity", + "session.cookies[].remember_me", + "session.cookies[]", + "session.cookies[].authelia_url", "session.redis.host", "session.redis.port", "session.redis.username", diff --git a/internal/configuration/schema/session.go b/internal/configuration/schema/session.go index 2a935ee9a..66719e202 100644 --- a/internal/configuration/schema/session.go +++ b/internal/configuration/schema/session.go @@ -2,6 +2,7 @@ package schema import ( "crypto/tls" + "net/url" "time" ) @@ -36,24 +37,42 @@ type RedisSessionConfiguration struct { // SessionConfiguration represents the configuration related to user sessions. type SessionConfiguration struct { - Name string `koanf:"name"` - Domain string `koanf:"domain"` - SameSite string `koanf:"same_site"` - Secret string `koanf:"secret"` - Expiration time.Duration `koanf:"expiration"` - Inactivity time.Duration `koanf:"inactivity"` - RememberMeDuration time.Duration `koanf:"remember_me_duration"` + Secret string `koanf:"secret"` + + SessionCookieCommonConfiguration `koanf:",squash"` + + Cookies []SessionCookieConfiguration `koanf:"cookies"` Redis *RedisSessionConfiguration `koanf:"redis"` } +type SessionCookieCommonConfiguration struct { + Name string `koanf:"name"` + Domain string `koanf:"domain"` + SameSite string `koanf:"same_site"` + Expiration time.Duration `koanf:"expiration"` + Inactivity time.Duration `koanf:"inactivity"` + RememberMe time.Duration `koanf:"remember_me"` + + DisableRememberMe bool +} + +// SessionCookieConfiguration represents the configuration for a cookie domain. +type SessionCookieConfiguration struct { + SessionCookieCommonConfiguration `koanf:",squash"` + + AutheliaURL *url.URL `koanf:"authelia_url"` +} + // DefaultSessionConfiguration is the default session configuration. var DefaultSessionConfiguration = SessionConfiguration{ - Name: "authelia_session", - Expiration: time.Hour, - Inactivity: time.Minute * 5, - RememberMeDuration: time.Hour * 24 * 30, - SameSite: "lax", + SessionCookieCommonConfiguration: SessionCookieCommonConfiguration{ + Name: "authelia_session", + Expiration: time.Hour, + Inactivity: time.Minute * 5, + RememberMe: time.Hour * 24 * 30, + SameSite: "lax", + }, } // DefaultRedisConfiguration is the default redis configuration. diff --git a/internal/configuration/test_resources/config_alt.yml b/internal/configuration/test_resources/config_alt.yml index 95c8fb89c..7f20e787e 100644 --- a/internal/configuration/test_resources/config_alt.yml +++ b/internal/configuration/test_resources/config_alt.yml @@ -97,7 +97,7 @@ session: name: authelia_session expiration: 3600000 # 1 hour inactivity: 300000 # 5 minutes - remember_me_duration: -1 + remember_me: -1 domain: example.com redis: host: 127.0.0.1 diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index 18ff2ae53..0abf16372 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -25,9 +25,15 @@ func newDefaultConfig() schema.Configuration { DefaultPolicy: "two_factor", } config.Session = schema.SessionConfiguration{ - Domain: examplecom, - Name: "authelia_session", Secret: "secret", + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", + Domain: exampleDotCom, + }, + }, + }, } config.Storage.EncryptionKey = testEncryptionKey config.Storage.Local = &schema.LocalStorageConfiguration{ @@ -49,6 +55,8 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) { ValidateConfiguration(&config, validator) require.Len(t, validator.Errors(), 0) + config = newDefaultConfig() + config.Notifier.SMTP = nil config.Notifier.FileSystem = nil @@ -135,6 +143,8 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { 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") + config = newDefaultConfig() + validator = schema.NewStructValidator() config.CertificatesDirectory = "const.go" diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index cf1a8eea0..9f68a32cb 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -248,7 +248,7 @@ const ( // Session error constants. const ( errFmtSessionOptionRequired = "session: option '%s' is required" - errFmtSessionDomainMustBeRoot = "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '%s'" + errFmtSessionLegacyAndWarning = "session: option 'domain' and option 'cookies' can't be specified at the same time" errFmtSessionSameSite = "session: option 'same_site' must be one of '%s' but is configured as '%s'" errFmtSessionSecretRequired = "session: option 'secret' is required when using the '%s' provider" errFmtSessionRedisPortRange = "session: redis: option 'port' must be between 1 and 65535 but is configured as '%d'" @@ -258,6 +258,16 @@ 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 is configured as '%s'" + errFmtSessionDomainSameSite = "session: domain config %s: option 'same_site' must be one of '%s' but is 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' is not a valid domain" ) // Regulation Error Consts. @@ -363,7 +373,10 @@ var ( validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} ) -var reKeyReplacer = regexp.MustCompile(`\[\d+]`) +var ( + reKeyReplacer = regexp.MustCompile(`\[\d+]`) + reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`) +) var replacedKeys = map[string]string{ "authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify", diff --git a/internal/configuration/validator/const_test.go b/internal/configuration/validator/const_test.go index ab398a187..1eea835ba 100644 --- a/internal/configuration/validator/const_test.go +++ b/internal/configuration/validator/const_test.go @@ -12,5 +12,5 @@ const ( ) const ( - examplecom = "example.com" + exampleDotCom = "example.com" ) diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index cd3bc0012..d0e345e97 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -257,7 +257,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { RedirectURIs: []string{ "https://google.com", }, - SectorIdentifier: mustParseURL(examplecom), + SectorIdentifier: mustParseURL(exampleDotCom), }, }, }, @@ -289,12 +289,12 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, }, Errors: []string{ - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "scheme", "https"), - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "path", "/path"), - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "query", "query=abc"), - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "fragment", "fragment"), - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "username", "user"), - fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "password"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "scheme", "https"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "path", "/path"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "query", "query=abc"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "fragment", "fragment"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "username", "user"), + fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "password"), }, }, { diff --git a/internal/configuration/validator/notifier_test.go b/internal/configuration/validator/notifier_test.go index 7ba756711..c41776a29 100644 --- a/internal/configuration/validator/notifier_test.go +++ b/internal/configuration/validator/notifier_test.go @@ -23,7 +23,7 @@ func (suite *NotifierSuite) SetupTest() { Username: "john", Password: "password", Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"}, - Host: examplecom, + Host: exampleDotCom, Port: 25, } suite.config.FileSystem = nil @@ -78,7 +78,7 @@ func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() { suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Errors(), 0) - suite.Assert().Equal(examplecom, suite.config.SMTP.TLS.ServerName) + suite.Assert().Equal(exampleDotCom, suite.config.SMTP.TLS.ServerName) suite.Assert().Equal(uint16(tls.VersionTLS12), suite.config.SMTP.TLS.MinimumVersion.Value) suite.Assert().False(suite.config.SMTP.TLS.SkipVerify) } @@ -111,7 +111,7 @@ func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() { } func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() { - suite.config.SMTP.Host = examplecom + suite.config.SMTP.Host = exampleDotCom suite.config.SMTP.TLS = &schema.TLSConfig{ MinimumVersion: schema.TLSVersion{Value: tls.VersionSSL30}, //nolint:staticcheck } @@ -125,7 +125,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() { } func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() { - suite.config.SMTP.Host = examplecom + suite.config.SMTP.Host = exampleDotCom suite.config.SMTP.TLS = &schema.TLSConfig{ MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13}, MaximumVersion: schema.TLSVersion{Value: tls.VersionTLS10}, @@ -140,7 +140,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() { } func (suite *NotifierSuite) TestSMTPShouldWarnOnDisabledSTARTTLS() { - suite.config.SMTP.Host = examplecom + suite.config.SMTP.Host = exampleDotCom suite.config.SMTP.DisableStartTLS = true ValidateNotifier(&suite.config, suite.validator) diff --git a/internal/configuration/validator/session.go b/internal/configuration/validator/session.go index 17917661d..08fdfeb0e 100644 --- a/internal/configuration/validator/session.go +++ b/internal/configuration/validator/session.go @@ -35,18 +35,11 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. } - if config.RememberMeDuration <= 0 && config.RememberMeDuration != schema.RememberMeDisabled { - config.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month. - } - - if config.Domain == "" { - validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "domain")) - } else if strings.HasPrefix(config.Domain, ".") { - validator.PushWarning(fmt.Errorf("session: 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")) - } - - if strings.HasPrefix(config.Domain, "*.") { - validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, config.Domain)) + switch { + case config.RememberMe == schema.RememberMeDisabled: + config.DisableRememberMe = true + case config.RememberMe <= 0: + config.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month. } if config.SameSite == "" { @@ -54,6 +47,137 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru } else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) { validator.Push(fmt.Errorf(errFmtSessionSameSite, strings.Join(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, "domain")) + } + + 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))) + case strings.HasPrefix(d.Domain, "*."): + validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain)) + case strings.HasPrefix(d.Domain, "."): + validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d))) + case !reDomainCharacters.MatchString(d.Domain): + validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, 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]), strings.Join(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) { diff --git a/internal/configuration/validator/session_test.go b/internal/configuration/validator/session_test.go index 5a08f988a..d677df581 100644 --- a/internal/configuration/validator/session_test.go +++ b/internal/configuration/validator/session_test.go @@ -3,7 +3,9 @@ package validator import ( "crypto/tls" "fmt" + "net/url" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,7 +16,8 @@ import ( func newDefaultSessionConfig() schema.SessionConfiguration { config := schema.SessionConfiguration{} config.Secret = testJWTSecret - config.Domain = examplecom + config.Domain = exampleDotCom + config.Cookies = []schema.SessionCookieConfiguration{} return config } @@ -30,15 +33,144 @@ func TestShouldSetDefaultSessionValues(t *testing.T) { 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.RememberMeDuration, config.RememberMeDuration) + assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe) assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite) } +func TestShouldSetDefaultSessionDomainsValues(t *testing.T) { + testCases := []struct { + name string + have schema.SessionConfiguration + expected schema.SessionConfiguration + errs []string + }{ + { + "ShouldSetGoodDefaultValues", + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + }, + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour, + Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + }, + }, + }, + nil, + }, + { + "ShouldNotSetBadDefaultValues", + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", Domain: exampleDotCom, + Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + }, + }, + }, + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", Domain: exampleDotCom, SameSite: schema.DefaultSessionConfiguration.SameSite, + Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2, + }, + }, + }, + }, + []string{ + "session: option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'BAD VALUE'", + }, + }, + { + "ShouldSetDefaultValuesForEachConfig", + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, + RememberMe: schema.RememberMeDisabled, + }, + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: exampleDotCom, + }, + }, + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "example2.com", Name: "authelia_session", SameSite: "strict", + }, + }, + }, + }, + schema.SessionConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, + RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, + }, + Cookies: []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "default_session", Domain: exampleDotCom, SameSite: "lax", + Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, + }, + }, + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", Domain: "example2.com", SameSite: "strict", + Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true, + }, + }, + }, + }, + nil, + }, + } + + validator := schema.NewStructValidator() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator.Clear() + + have := tc.have + + ValidateSession(&have, validator) + + assert.Len(t, validator.Warnings(), 0) + + errs := validator.Errors() + require.Len(t, validator.Errors(), len(tc.errs)) + + for i, err := range errs { + assert.EqualError(t, err, tc.errs[i]) + } + + assert.Equal(t, tc.expected, have) + }) + } +} + func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) { validator := schema.NewStructValidator() config := newDefaultSessionConfig() - config.Expiration, config.Inactivity, config.RememberMeDuration = -1, -1, -2 + config.Expiration, config.Inactivity, config.RememberMe = -1, -1, -2 ValidateSession(&config, validator) @@ -46,7 +178,7 @@ func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) { 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.RememberMeDuration, config.RememberMeDuration) + assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe) } func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) { @@ -60,7 +192,7 @@ func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) { require.Len(t, validator.Warnings(), 1) assert.Len(t, validator.Errors(), 0) - assert.EqualError(t, validator.Warnings()[0], "session: 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") + assert.EqualError(t, validator.Warnings()[0], "session: domain config #1 (domain '.example.com'): 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") } func TestShouldHandleRedisConfigSuccessfully(t *testing.T) { @@ -72,6 +204,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) { assert.Len(t, validator.Errors(), 0) validator.Clear() + config = newDefaultSessionConfig() + // Set redis config because password must be set only when redis is used. config.Redis = &schema.RedisSessionConfiguration{ Host: "redis.localhost", @@ -81,8 +215,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) { ValidateSession(&config, validator) - assert.False(t, validator.HasWarnings()) - assert.False(t, validator.HasErrors()) + assert.Len(t, validator.Warnings(), 0) + assert.Len(t, validator.Errors(), 0) assert.Equal(t, 8, config.Redis.MaximumActiveConnections) } @@ -98,7 +232,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) { ValidateSession(&config, validator) - assert.False(t, validator.HasWarnings()) + require.Len(t, validator.Warnings(), 0) require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, -1)) @@ -131,6 +265,9 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) { assert.Len(t, validator.Errors(), 0) validator.Clear() + config = newDefaultSessionConfig() + config.Secret = "" + // Set redis config because password must be set only when redis is used. config.Redis = &schema.RedisSessionConfiguration{ Host: "redis.localhost", @@ -153,6 +290,8 @@ func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) { assert.Len(t, validator.Errors(), 0) validator.Clear() + config = newDefaultSessionConfig() + // Set redis config because password must be set only when redis is used. config.Redis = &schema.RedisSessionConfiguration{ Host: "redis.localhost", @@ -287,7 +426,24 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi validator.Clear() - config.Redis.Port = -1 + config = newDefaultSessionConfig() + + config.Secret = "" + config.Redis = &schema.RedisSessionConfiguration{ + Port: -1, + HighAvailability: &schema.RedisHighAvailabilityConfiguration{ + SentinelName: "sentinel", + SentinelPassword: "abc123", + Nodes: []schema.RedisNode{ + { + Host: "node1", + Port: 26379, + }, + }, + RouteByLatency: true, + RouteRandomly: true, + }, + } ValidateSession(&config, validator) @@ -434,6 +590,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) { validator := schema.NewStructValidator() config := newDefaultSessionConfig() config.Domain = "" + config.Cookies = []schema.SessionCookieConfiguration{} ValidateSession(&config, validator) @@ -449,9 +606,141 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) { ValidateSession(&config, validator) + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), 1) + + assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain '*.example.com'): option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'") +} + +func TestShouldRaiseErrorWhenDomainNameIsInvalid(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + config.Domain = "example!.com" + + ValidateSession(&config, validator) + + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), 1) + + assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example!.com'): option 'domain' is not a valid domain") +} + +func TestShouldRaiseErrorWhenHaveDuplicatedDomainName(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + config.Domain = "" + config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: exampleDotCom, + }, + AutheliaURL: MustParseURL("https://login.example.com"), + }) + config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + 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], "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'") + assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(1, schema.SessionCookieConfiguration{SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{Domain: exampleDotCom}}))) +} + +func TestShouldRaiseErrorWhenSubdomainConflicts(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + config.Domain = "" + config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: exampleDotCom, + }, + AutheliaURL: MustParseURL("https://login.example.com"), + }) + config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{ + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + 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) + 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") +} + +func TestShouldRaiseErrorWhenDomainIsInvalid(t *testing.T) { + testCases := []struct { + name string + have string + expected []string + }{ + {"ShouldRaiseErrorOnMissingDomain", "", []string{"session: domain config #1 (domain ''): option 'domain' is required"}}, + {"ShouldNotRaiseErrorOnValidDomain", exampleDotCom, nil}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + config.Domain = "" + + config.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: tc.have, + }, + AutheliaURL: MustParseURL("https://auth.example.com")}, + } + + ValidateSession(&config, validator) + + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), len(tc.expected)) + + for i, expected := range tc.expected { + assert.EqualError(t, validator.Errors()[i], expected) + } + }) + } +} + +func TestShouldRaiseErrorWhenPortalURLIsInvalid(t *testing.T) { + testCases := []struct { + name string + have string + expected []string + }{ + {"ShouldRaiseErrorOnInvalidScope", "https://example2.com/login", []string{"session: domain config #1 (domain 'example.com'): option 'authelia_url' does not share a cookie scope with domain 'example.com' with a value of 'https://example2.com/login'"}}, + {"ShouldRaiseErrorOnInvalidScheme", "http://example.com/login", []string{"session: domain config #1 (domain 'example.com'): option 'authelia_url' does not have a secure scheme with a value of 'http://example.com/login'"}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + config.Domain = "" + config.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", + Domain: exampleDotCom, + }, + AutheliaURL: MustParseURL(tc.have)}, + } + + ValidateSession(&config, validator) + + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), len(tc.expected)) + + for i, expected := range tc.expected { + assert.EqualError(t, validator.Errors()[i], expected) + } + }) + } } func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) { @@ -462,22 +751,28 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) { ValidateSession(&config, validator) assert.False(t, validator.HasWarnings()) - assert.Len(t, validator.Errors(), 1) + require.Len(t, validator.Errors(), 2) + assert.EqualError(t, validator.Errors()[0], "session: option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'NOne'") + assert.EqualError(t, validator.Errors()[1], "session: domain config #1 (domain 'example.com'): option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'NOne'") } func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) { validator := schema.NewStructValidator() - config := newDefaultSessionConfig() + + var config schema.SessionConfiguration validOptions := []string{"none", "lax", "strict"} for _, opt := range validOptions { + validator.Clear() + + config = newDefaultSessionConfig() config.SameSite = opt ValidateSession(&config, validator) - assert.False(t, validator.HasWarnings()) + assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Errors(), 0) } } @@ -487,7 +782,7 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing config := newDefaultSessionConfig() config.Inactivity = -1 config.Expiration = -1 - config.RememberMeDuration = schema.RememberMeDisabled + config.RememberMe = schema.RememberMeDisabled ValidateSession(&config, validator) @@ -496,7 +791,8 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) - assert.Equal(t, schema.RememberMeDisabled, config.RememberMeDuration) + assert.Equal(t, schema.RememberMeDisabled, config.RememberMe) + assert.True(t, config.DisableRememberMe) } func TestShouldSetDefaultRememberMeDuration(t *testing.T) { @@ -505,7 +801,41 @@ func TestShouldSetDefaultRememberMeDuration(t *testing.T) { ValidateSession(&config, validator) - assert.False(t, validator.HasWarnings()) - assert.False(t, validator.HasErrors()) - assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration) + assert.Len(t, validator.Warnings(), 0) + assert.Len(t, validator.Errors(), 0) + + assert.Equal(t, config.RememberMe, schema.DefaultSessionConfiguration.RememberMe) +} + +func TestShouldNotAllowLegacyAndModernCookiesConfig(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultSessionConfig() + + 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, + }, + }) + + ValidateSession(&config, validator) + + assert.Len(t, validator.Warnings(), 0) + require.Len(t, validator.Errors(), 1) + + assert.EqualError(t, validator.Errors()[0], "session: option 'domain' and option 'cookies' can't be specified at the same time") +} + +func MustParseURL(uri string) *url.URL { + u, err := url.ParseRequestURI(uri) + + if err != nil { + panic(err) + } + + return u } diff --git a/internal/handlers/const.go b/internal/handlers/const.go index ae9e4d603..2403bf779 100644 --- a/internal/handlers/const.go +++ b/internal/handlers/const.go @@ -112,6 +112,7 @@ const ( testInactivity = time.Second * 10 testRedirectionURL = "http://redirection.local" testUsername = "john" + exampleDotCom = "example.com" ) // Duo constants. diff --git a/internal/handlers/handler_checks_safe_redirection.go b/internal/handlers/handler_checks_safe_redirection.go index 96b66114e..be1eef04d 100644 --- a/internal/handlers/handler_checks_safe_redirection.go +++ b/internal/handlers/handler_checks_safe_redirection.go @@ -2,9 +2,9 @@ package handlers import ( "fmt" + "net/url" "github.com/authelia/authelia/v4/internal/middlewares" - "github.com/authelia/authelia/v4/internal/utils" ) // CheckSafeRedirectionPOST handler checking whether the redirection to a given URL provided in body is safe. @@ -16,24 +16,23 @@ func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) { return } - var reqBody checkURIWithinDomainRequestBody + var ( + bodyJSON checkURIWithinDomainRequestBody + targetURI *url.URL + err error + ) - err := ctx.ParseBody(&reqBody) - if err != nil { + if err = ctx.ParseBody(&bodyJSON); err != nil { ctx.Error(fmt.Errorf("unable to parse request body: %w", err), messageOperationFailed) return } - safe, err := utils.IsURIStringSafeRedirection(reqBody.URI, ctx.Configuration.Session.Domain) - if err != nil { - ctx.Error(fmt.Errorf("unable to determine if uri %s is safe to redirect to: %w", reqBody.URI, err), messageOperationFailed) + if targetURI, err = url.ParseRequestURI(bodyJSON.URI); err != nil { + ctx.Error(fmt.Errorf("unable to determine if uri %s is safe to redirect to: failed to parse URI '%s': %w", bodyJSON.URI, bodyJSON.URI, err), messageOperationFailed) return } - err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{ - OK: safe, - }) - if err != nil { + if err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{OK: ctx.IsSafeRedirectionTargetURI(targetURI)}); err != nil { ctx.Error(fmt.Errorf("unable to create response body: %w", err), messageOperationFailed) return } diff --git a/internal/handlers/handler_checks_safe_redirection_test.go b/internal/handlers/handler_checks_safe_redirection_test.go index a77fefabb..cb5ec41de 100644 --- a/internal/handlers/handler_checks_safe_redirection_test.go +++ b/internal/handlers/handler_checks_safe_redirection_test.go @@ -4,64 +4,78 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/session" ) -var exampleDotComDomain = "example.com" +func TestCheckSafeRedirection(t *testing.T) { + testCases := []struct { + name string + userSession session.UserSession + have string + expected int + ok bool + }{ + { + "ShouldReturnUnauthorized", + session.UserSession{AuthenticationLevel: authentication.NotAuthenticated}, + "http://myapp.example.com", + fasthttp.StatusUnauthorized, + false, + }, + { + "ShouldReturnTrueOnGoodDomain", + session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor}, + "https://myapp.example.com", + fasthttp.StatusOK, + true, + }, + { + "ShouldReturnFalseOnGoodDomainWithBadScheme", + session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor}, + "http://myapp.example.com", + fasthttp.StatusOK, + false, + }, + { + "ShouldReturnFalseOnBadDomainWithGoodScheme", + session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor}, + "https://myapp.notgood.com", + fasthttp.StatusOK, + false, + }, + { + "ShouldReturnFalseOnBadDomainWithBadScheme", + session.UserSession{Username: "john", AuthenticationLevel: authentication.OneFactor}, + "http://myapp.notgood.com", + fasthttp.StatusOK, + false, + }, + } -func TestCheckSafeRedirection_ForbiddenCall(t *testing.T) { - mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{ - Username: "john", - AuthenticationLevel: authentication.NotAuthenticated, - }) - defer mock.Close() - mock.Ctx.Configuration.Session.Domain = exampleDotComDomain + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mock := mocks.NewMockAutheliaCtxWithUserSession(t, tc.userSession) + defer mock.Close() - mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ - URI: "http://myapp.example.com", - }) + mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ + URI: tc.have, + }) - CheckSafeRedirectionPOST(mock.Ctx) - assert.Equal(t, 401, mock.Ctx.Response.StatusCode()) -} + CheckSafeRedirectionPOST(mock.Ctx) -func TestCheckSafeRedirection_UnsafeRedirection(t *testing.T) { - mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{ - Username: "john", - AuthenticationLevel: authentication.OneFactor, - }) - defer mock.Close() - mock.Ctx.Configuration.Session.Domain = exampleDotComDomain + assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode()) - mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ - URI: "http://myapp.com", - }) - - CheckSafeRedirectionPOST(mock.Ctx) - mock.Assert200OK(t, checkURIWithinDomainResponseBody{ - OK: false, - }) -} - -func TestCheckSafeRedirection_SafeRedirection(t *testing.T) { - mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{ - Username: "john", - AuthenticationLevel: authentication.OneFactor, - }) - defer mock.Close() - mock.Ctx.Configuration.Session.Domain = exampleDotComDomain - - mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ - URI: "https://myapp.example.com", - }) - - CheckSafeRedirectionPOST(mock.Ctx) - mock.Assert200OK(t, checkURIWithinDomainResponseBody{ - OK: true, - }) + if tc.expected == fasthttp.StatusOK { + mock.Assert200OK(t, checkURIWithinDomainResponseBody{ + OK: tc.ok, + }) + } + }) + } } func TestShouldFailOnInvalidBody(t *testing.T) { @@ -70,7 +84,7 @@ func TestShouldFailOnInvalidBody(t *testing.T) { AuthenticationLevel: authentication.OneFactor, }) defer mock.Close() - mock.Ctx.Configuration.Session.Domain = exampleDotComDomain + mock.Ctx.Configuration.Session.Domain = exampleDotCom mock.SetRequestBody(t, "not a valid json") @@ -84,7 +98,7 @@ func TestShouldFailOnInvalidURL(t *testing.T) { AuthenticationLevel: authentication.OneFactor, }) defer mock.Close() - mock.Ctx.Configuration.Session.Domain = exampleDotComDomain + mock.Ctx.Configuration.Session.Domain = exampleDotCom mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ URI: "https//invalid-url", diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index f52119576..3f896fb28 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -4,7 +4,6 @@ import ( "errors" "time" - "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/session" @@ -72,7 +71,25 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re return } - userSession := ctx.GetSession() + // TODO: write tests. + provider, err := ctx.GetSessionProvider() + if err != nil { + ctx.Logger.Errorf("%s", err) + + respondUnauthorized(ctx, messageAuthenticationFailed) + + return + } + + userSession, err := provider.GetSession(ctx.RequestCtx) + if err != nil { + ctx.Logger.Errorf("%s", err) + + respondUnauthorized(ctx, messageAuthenticationFailed) + + return + } + newSession := session.NewDefaultUserSession() // Reset all values from previous session except OIDC workflow before regenerating the cookie. @@ -84,7 +101,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re return } - if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil { + if err = ctx.RegenerateSession(); err != nil { ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthType1FA, bodyJSON.Username, err) respondUnauthorized(ctx, messageAuthenticationFailed) @@ -93,11 +110,11 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re } // Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data. - keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != schema.RememberMeDisabled && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn + keepMeLoggedIn := !provider.Config.DisableRememberMe && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn // Set the cookie to expire if remember me is enabled and the user has asked us to. if keepMeLoggedIn { - err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) + err = provider.UpdateExpiration(ctx.RequestCtx, provider.Config.RememberMe) if err != nil { ctx.Logger.Errorf(logFmtErrSessionSave, "updated expiration", regulation.AuthType1FA, bodyJSON.Username, err) diff --git a/internal/handlers/handler_logout.go b/internal/handlers/handler_logout.go index 0f491dd87..7401c8baf 100644 --- a/internal/handlers/handler_logout.go +++ b/internal/handlers/handler_logout.go @@ -5,7 +5,6 @@ import ( "net/url" "github.com/authelia/authelia/v4/internal/middlewares" - "github.com/authelia/authelia/v4/internal/utils" ) type logoutBody struct { @@ -26,14 +25,14 @@ func LogoutPOST(ctx *middlewares.AutheliaCtx) { ctx.Error(fmt.Errorf("unable to parse body during logout: %s", err), messageOperationFailed) } - err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx) + err = ctx.DestroySession() if err != nil { ctx.Error(fmt.Errorf("unable to destroy session during logout: %s", err), messageOperationFailed) } redirectionURL, err := url.ParseRequestURI(body.TargetURL) if err == nil { - responseBody.SafeTargetURL = utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) + responseBody.SafeTargetURL = ctx.IsSafeRedirectionTargetURI(redirectionURL) } if body.TargetURL != "" { diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go index 0ebe285b0..609edca76 100644 --- a/internal/handlers/handler_sign_duo.go +++ b/internal/handlers/handler_sign_duo.go @@ -246,7 +246,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) { userSession := ctx.GetSession() - err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) + err := ctx.RegenerateSession() if err != nil { ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username, err) diff --git a/internal/handlers/handler_sign_duo_test.go b/internal/handlers/handler_sign_duo_test.go index c08c8f1ee..70765bf71 100644 --- a/internal/handlers/handler_sign_duo_test.go +++ b/internal/handlers/handler_sign_duo_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/duo" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/model" @@ -579,6 +580,18 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() { func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() { duoMock := mocks.NewMockAPI(s.mock.Ctrl) + s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "example.com", + }, + }, + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "mydomain.local", + }, + }, + } s.mock.StorageMock.EXPECT(). LoadPreferredDuoDevice(s.mock.Ctx, "john"). Return(&model.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go index 3d584fddd..1825dcb84 100644 --- a/internal/handlers/handler_sign_totp.go +++ b/internal/handlers/handler_sign_totp.go @@ -50,7 +50,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) { return } - if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil { + if err = ctx.RegenerateSession(); err != nil { ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeTOTP, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go index 6ade2e0dc..b4cd12086 100644 --- a/internal/handlers/handler_sign_totp_test.go +++ b/internal/handlers/handler_sign_totp_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/regulation" @@ -143,6 +144,18 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() { config := model.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"} + s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "example.com", + }, + }, + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Domain: "mydomain.local", + }, + }, + } s.mock.StorageMock.EXPECT(). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). diff --git a/internal/handlers/handler_sign_webauthn.go b/internal/handlers/handler_sign_webauthn.go index 7f6f3f649..571f2d8cf 100644 --- a/internal/handlers/handler_sign_webauthn.go +++ b/internal/handlers/handler_sign_webauthn.go @@ -171,7 +171,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) { return } - if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil { + if err = ctx.RegenerateSession(); err != nil { ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) diff --git a/internal/handlers/handler_user_info_test.go b/internal/handlers/handler_user_info_test.go index 47bc9a6b8..1fb8e2a83 100644 --- a/internal/handlers/handler_user_info_test.go +++ b/internal/handlers/handler_user_info_test.go @@ -259,9 +259,11 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) { } mock := mocks.NewMockAutheliaCtx(t) + sessionConfig := mock.Ctx.Configuration.Session if resp.config != nil { mock.Ctx.Configuration = *resp.config + mock.Ctx.Configuration.Session = sessionConfig } // Set the initial user session. diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go index 258aec61d..723a05c69 100644 --- a/internal/handlers/handler_verify.go +++ b/internal/handlers/handler_verify.go @@ -19,14 +19,6 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) -func isURLUnderProtectedDomain(url *url.URL, domain string) bool { - return strings.HasSuffix(url.Hostname(), domain) -} - -func isSchemeHTTPS(url *url.URL) bool { - return url.Scheme == "https" -} - func isSchemeWSS(url *url.URL) bool { return url.Scheme == "wss" } @@ -54,7 +46,7 @@ func parseBasicAuth(header []byte, auth string) (username, password string, err } // isTargetURLAuthorized check whether the given user is authorized to access the resource. -func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.URL, +func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL *url.URL, username string, userGroups []string, clientIP net.IP, method []byte, authLevel authentication.Level) authorizationMatching { hasSubject, level := authorizer.GetRequiredLevel( authorization.Subject{ @@ -62,7 +54,7 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U Groups: userGroups, IP: clientIP, }, - authorization.NewObjectRaw(&targetURL, method)) + authorization.NewObjectRaw(targetURL, method)) switch { case level == authorization.Bypass: @@ -128,13 +120,18 @@ func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string } func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isUserAnonymous bool) (isInactiveTooLong bool) { - if userSession.KeepMeLoggedIn || isUserAnonymous || int64(ctx.Providers.SessionProvider.Inactivity.Seconds()) == 0 { + domainSession, err := ctx.GetSessionProvider() + if err != nil { return false } - isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(ctx.Providers.SessionProvider.Inactivity).Before(ctx.Clock.Now()) + if userSession.KeepMeLoggedIn || isUserAnonymous || int64(domainSession.Config.Inactivity.Seconds()) == 0 { + return false + } - ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(ctx.Providers.SessionProvider.Inactivity.Seconds())) + isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(domainSession.Config.Inactivity).Before(ctx.Clock.Now()) + + ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(domainSession.Config.Inactivity.Seconds())) return isInactiveTooLong } @@ -151,7 +148,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) { // Destroy the session a new one will be regenerated on next request. - if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil { + if err = ctx.DestroySession(); err != nil { return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy session for user '%s' after the session has been inactive too long: %w", userSession.Username, err) } @@ -162,7 +159,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS if err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil { if err == authentication.ErrUserNotFound { - if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil { + if err = ctx.DestroySession(); err != nil { ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %v", err) } @@ -177,7 +174,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, userSession.AuthenticationLevel, nil } -func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) { +func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, cookieDomain string, isBasicAuth bool, username string, method []byte) { var ( statusCode int friendlyUsername string @@ -211,8 +208,8 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is redirectionURL := ctxGetPortalURL(ctx) if redirectionURL != nil { - if !utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) { - ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, ctx.Configuration.Session.Domain) + if !utils.IsURISafeRedirection(redirectionURL, cookieDomain) { + ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, cookieDomain) ctx.ReplyUnauthorized() @@ -414,6 +411,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile } userSession := ctx.GetSession() + if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil { return isBasicAuth, username, name, groups, emails, authLevel, err } @@ -422,7 +420,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) { ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response") - if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil { + if err = ctx.DestroySession(); err != nil { ctx.Logger.Errorf("Unable to destroy user session after handler could not match them to their %s header: %s", headerSessionUsername, err) } @@ -447,7 +445,7 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler { return } - if !isSchemeHTTPS(targetURL) && !isSchemeWSS(targetURL) { + if !utils.IsURISecure(targetURL) { ctx.Logger.Errorf("Scheme of target URL %s must be secure since cookies are "+ "only transported over a secure connection for security reasons", targetURL.String()) ctx.ReplyUnauthorized() @@ -455,14 +453,32 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler { return } - if !isURLUnderProtectedDomain(targetURL, ctx.Configuration.Session.Domain) { - ctx.Logger.Errorf("Target URL %s is not under the protected domain %s", - targetURL.String(), ctx.Configuration.Session.Domain) + cookieDomain := ctx.GetTargetURICookieDomain(targetURL) + + if cookieDomain == "" { + l := len(ctx.Configuration.Session.Cookies) + + if l == 1 { + ctx.Logger.Errorf("Target URL '%s' was not detected as a match to the '%s' session cookie domain", + targetURL.String(), ctx.Configuration.Session.Cookies[0].Domain) + } else { + domains := make([]string, 0, len(ctx.Configuration.Session.Cookies)) + + for i, domain := range ctx.Configuration.Session.Cookies { + domains[i] = domain.Domain + } + + ctx.Logger.Errorf("Target URL '%s' was not detected as a match to any of the '%s' session cookie domains", + targetURL.String(), strings.Join(domains, "', '")) + } + ctx.ReplyUnauthorized() return } + ctx.Logger.Debugf("Target URL '%s' was detected as a match to the '%s' session cookie domain", targetURL.String(), cookieDomain) + method := ctx.XForwardedMethod() isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval) @@ -474,12 +490,12 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler { return } - handleUnauthorized(ctx, targetURL, isBasicAuth, username, method) + handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method) return } - authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, *targetURL, username, + authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, targetURL, username, groups, ctx.RemoteIP(), method, authLevel) switch authorized { @@ -487,7 +503,7 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler { ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username) ctx.ReplyForbidden() case NotAuthorized: - handleUnauthorized(ctx, targetURL, isBasicAuth, username, method) + handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method) case Authorized: setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails) } diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go index 518ac7ec0..20cbf8f22 100644 --- a/internal/handlers/handler_verify_test.go +++ b/internal/handlers/handler_verify_test.go @@ -43,6 +43,8 @@ func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) { func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) + mock.Ctx.Request.Header.Del("X-Forwarded-Host") + defer mock.Close() _, err := mock.Ctx.GetOriginalURL() assert.Error(t, err) @@ -53,6 +55,7 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() + mock.Ctx.Request.Header.Del("X-Forwarded-Host") mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") _, err := mock.Ctx.GetOriginalURL() assert.Error(t, err) @@ -162,7 +165,7 @@ func TestShouldCheckAuthorizationMatching(t *testing.T) { username = testUsername } - matching := isTargetURLAuthorized(authorizer, *u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel) + matching := isTargetURLAuthorized(authorizer, u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel) assert.Equal(t, rule.ExpectedMatching, matching, "policy=%s, authLevel=%v, expected=%v, actual=%v", rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching) } @@ -510,7 +513,6 @@ func TestShouldNotCrashOnEmptyEmail(t *testing.T) { userSession.AuthenticationLevel = authentication.OneFactor userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) - fmt.Printf("Time is %v\n", userSession.RefreshTTL) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) @@ -663,32 +665,33 @@ func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) { {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403}, } - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.String(), func(t *testing.T) { + for i, tc := range testCases { + t.Run(tc.String(), func(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() mock.Clock.Set(time.Now()) + mock.Ctx.Request.Header.Set("X-Original-URL", tc.URL) + userSession := mock.Ctx.GetSession() - userSession.Username = testCase.Username - userSession.Emails = testCase.Emails - userSession.AuthenticationLevel = testCase.AuthenticationLevel + userSession.Username = tc.Username + userSession.Emails = tc.Emails + userSession.AuthenticationLevel = tc.AuthenticationLevel userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) - mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL) - VerifyGET(verifyGetCfg)(mock.Ctx) - expStatus, actualStatus := testCase.ExpectedStatusCode, mock.Ctx.Response.StatusCode() + expStatus, actualStatus := tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode() assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d", - testCase.URL, testCase.AuthenticationLevel, actualStatus, expStatus) + tc.URL, tc.AuthenticationLevel, actualStatus, expStatus) - if testCase.ExpectedStatusCode == 200 && testCase.Username != "" { - assert.Equal(t, []byte(testCase.Username), mock.Ctx.Response.Header.Peek("Remote-User")) + fmt.Println(i) + if tc.ExpectedStatusCode == 200 && tc.Username != "" { + assert.Equal(t, tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode()) + assert.Equal(t, []byte(tc.Username), mock.Ctx.Response.Header.Peek("Remote-User")) assert.Equal(t, []byte("john.doe@example.com"), mock.Ctx.Response.Header.Peek("Remote-Email")) } else { assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User")) @@ -706,10 +709,12 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) { clock.Set(time.Now()) past := clock.Now().Add(-1 * time.Hour) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) - assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) + assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity) + + mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") userSession := mock.Ctx.GetSession() userSession.Username = testUsername @@ -719,8 +724,6 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) { err := mock.Ctx.SaveSession(userSession) require.NoError(t, err) - mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com") - VerifyGET(verifyGetCfg)(mock.Ctx) // The session has been destroyed. @@ -739,10 +742,10 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test clock := utils.TestingClock{} clock.Set(time.Now()) - mock.Ctx.Configuration.Session.Inactivity = time.Second * 10 + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = time.Second * 10 // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) - assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) + assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity) userSession := mock.Ctx.GetSession() userSession.Username = testUsername @@ -768,7 +771,7 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te mock.Clock.Set(time.Now()) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity userSession := mock.Ctx.GetSession() userSession.Username = testUsername @@ -800,7 +803,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T) mock.Clock.Set(time.Now()) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity past := mock.Clock.Now().Add(-1 * time.Hour) @@ -836,10 +839,10 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin clock := utils.TestingClock{} clock.Set(time.Now()) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) - assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) + assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity) past := clock.Now().Add(-1 * time.Hour) @@ -899,7 +902,7 @@ func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *tes mock.Clock.Set(time.Now()) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity past := mock.Clock.Now().Add(-1 * time.Hour) @@ -974,47 +977,6 @@ func TestShouldURLEncodeRedirectionHeader(t *testing.T) { string(mock.Ctx.Response.Body())) } -func TestIsDomainProtected(t *testing.T) { - GetURL := func(u string) *url.URL { - x, err := url.ParseRequestURI(u) - require.NoError(t, err) - - return x - } - - assert.True(t, isURLUnderProtectedDomain( - GetURL("http://mytest.example.com/abc/?query=abc"), "example.com")) - - assert.True(t, isURLUnderProtectedDomain( - GetURL("http://example.com/abc/?query=abc"), "example.com")) - - assert.True(t, isURLUnderProtectedDomain( - GetURL("https://mytest.example.com/abc/?query=abc"), "example.com")) - - // Cookies readable by a service on a machine is also readable by a service on the same machine - // with a different port as mentioned in https://tools.ietf.org/html/rfc6265#section-8.5. - assert.True(t, isURLUnderProtectedDomain( - GetURL("https://mytest.example.com:8080/abc/?query=abc"), "example.com")) -} - -func TestSchemeIsHTTPS(t *testing.T) { - GetURL := func(u string) *url.URL { - x, err := url.ParseRequestURI(u) - require.NoError(t, err) - - return x - } - - assert.False(t, isSchemeHTTPS( - GetURL("http://mytest.example.com/abc/?query=abc"))) - assert.False(t, isSchemeHTTPS( - GetURL("ws://mytest.example.com/abc/?query=abc"))) - assert.False(t, isSchemeHTTPS( - GetURL("wss://mytest.example.com/abc/?query=abc"))) - assert.True(t, isSchemeHTTPS( - GetURL("https://mytest.example.com/abc/?query=abc"))) -} - func TestSchemeIsWSS(t *testing.T) { GetURL := func(u string) *url.URL { x, err := url.ParseRequestURI(u) @@ -1435,10 +1397,10 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing. clock.Set(time.Now()) past := clock.Now().Add(-1 * time.Hour) - mock.Ctx.Configuration.Session.Inactivity = testInactivity + mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity // Reload the session provider since the configuration is indirect. mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) - assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) + assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity) userSession := mock.Ctx.GetSession() userSession.Username = testUsername @@ -1527,7 +1489,7 @@ func TestIsSessionInactiveTooLong(t *testing.T) { defer ctx.Close() - ctx.Ctx.Configuration.Session.Inactivity = tc.inactivity + ctx.Ctx.Configuration.Session.Cookies[0].Inactivity = tc.inactivity ctx.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil) ctx.Clock.Set(tc.now) diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 8669597f1..89e964630 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -13,7 +13,6 @@ import ( "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/oidc" - "github.com/authelia/authelia/v4/internal/utils" ) // Handle1FAResponse handle the redirection upon 1FA authentication. @@ -57,7 +56,7 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st return } - if !utils.IsURISafeRedirection(targetURL, ctx.Configuration.Session.Domain) { + if !ctx.IsSafeRedirectionTargetURI(targetURL) { ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI) if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { @@ -98,14 +97,18 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) { return } - var safe bool - - if safe, err = utils.IsURIStringSafeRedirection(targetURI, ctx.Configuration.Session.Domain); err != nil { - ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed) + var ( + parsedURI *url.URL + safe bool + ) + if parsedURI, err = url.ParseRequestURI(targetURI); err != nil { + ctx.Error(fmt.Errorf("unable to determine if URI '%s' is safe to redirect to: failed to parse URI '%s': %w", targetURI, targetURI, err), messageMFAValidationFailed) return } + safe = ctx.IsSafeRedirectionTargetURI(parsedURI) + if safe { ctx.Logger.Debugf("Redirection URL %s is safe", targetURI) diff --git a/internal/handlers/webauthn_test.go b/internal/handlers/webauthn_test.go index 70f68457c..7695adc3b 100644 --- a/internal/handlers/webauthn_test.go +++ b/internal/handlers/webauthn_test.go @@ -146,6 +146,7 @@ func TestWebauthnGetUserWithErr(t *testing.T) { func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) { ctx := mocks.NewMockAutheliaCtx(t) + ctx.Ctx.Request.Header.Del("X-Forwarded-Host") w, err := newWebauthn(ctx.Ctx) diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go index 093fd7000..036517964 100644 --- a/internal/middlewares/authelia_context.go +++ b/internal/middlewares/authelia_context.go @@ -227,9 +227,63 @@ func (ctx *AutheliaCtx) RootURLSlash() (issuerURL *url.URL) { } } +// GetTargetURICookieDomain returns the session provider for the targetURI domain. +func (ctx *AutheliaCtx) GetTargetURICookieDomain(targetURI *url.URL) string { + hostname := targetURI.Hostname() + + for _, domain := range ctx.Configuration.Session.Cookies { + if utils.HasDomainSuffix(hostname, domain.Domain) { + return domain.Domain + } + } + + return "" +} + +// IsSafeRedirectionTargetURI returns true if the targetURI is within the scope of a cookie domain and secure. +func (ctx *AutheliaCtx) IsSafeRedirectionTargetURI(targetURI *url.URL) bool { + if !utils.IsURISecure(targetURI) { + return false + } + + return ctx.GetTargetURICookieDomain(targetURI) != "" +} + +// GetCookieDomain returns the cookie domain for the current request. +func (ctx *AutheliaCtx) GetCookieDomain() (domain string, err error) { + var targetURI *url.URL + + if targetURI, err = ctx.GetOriginalURL(); err != nil { + return "", fmt.Errorf("unable to retrieve cookie domain: %s", err) + } + + return ctx.GetTargetURICookieDomain(targetURI), nil +} + +// GetSessionProvider returns the session provider for the Request's domain. +func (ctx *AutheliaCtx) GetSessionProvider() (provider *session.Session, err error) { + var cookieDomain string + + if cookieDomain, err = ctx.GetCookieDomain(); err != nil { + return nil, err + } + + if cookieDomain == "" { + return nil, fmt.Errorf("unable to retrieve domain session: %s", err) + } + + return ctx.Providers.SessionProvider.Get(cookieDomain) +} + // GetSession return the user session. Any update will be saved in cache. func (ctx *AutheliaCtx) GetSession() session.UserSession { - userSession, err := ctx.Providers.SessionProvider.GetSession(ctx.RequestCtx) + provider, err := ctx.GetSessionProvider() + if err != nil { + ctx.Logger.Error("Unable to retrieve domain session") + return session.NewDefaultUserSession() + } + + userSession, err := provider.GetSession(ctx.RequestCtx) if err != nil { ctx.Logger.Error("Unable to retrieve user session") return session.NewDefaultUserSession() @@ -240,7 +294,32 @@ func (ctx *AutheliaCtx) GetSession() session.UserSession { // SaveSession save the content of the session. func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error { - return ctx.Providers.SessionProvider.SaveSession(ctx.RequestCtx, userSession) + provider, err := ctx.GetSessionProvider() + if err != nil { + return fmt.Errorf("unable to save user session: %s", err) + } + + return provider.SaveSession(ctx.RequestCtx, userSession) +} + +// RegenerateSession regenerates user session. +func (ctx *AutheliaCtx) RegenerateSession() error { + provider, err := ctx.GetSessionProvider() + if err != nil { + return fmt.Errorf("unable to regenerate user session: %s", err) + } + + return provider.RegenerateSession(ctx.RequestCtx) +} + +// DestroySession destroy user session. +func (ctx *AutheliaCtx) DestroySession() error { + provider, err := ctx.GetSessionProvider() + if err != nil { + return fmt.Errorf("unable to destroy user session: %s", err) + } + + return provider.DestroySession(ctx.RequestCtx) } // ReplyOK is a helper method to reply ok. diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go index 57ac0f613..abf63b4dd 100644 --- a/internal/middlewares/authelia_context_test.go +++ b/internal/middlewares/authelia_context_test.go @@ -182,6 +182,8 @@ func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) { func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) + mock.Ctx.Request.Header.Del("X-Forwarded-Host") + defer mock.Close() mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password") @@ -196,6 +198,8 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) { mock := mocks.NewMockAutheliaCtx(t) defer mock.Close() + mock.Ctx.Request.Header.Del("X-Forwarded-Host") + mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password") mock.Ctx.RequestCtx.Request.SetHost("localhost") mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234") diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go index b7fef421c..17f742648 100644 --- a/internal/mocks/authelia_ctx.go +++ b/internal/mocks/authelia_ctx.go @@ -51,9 +51,25 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { mockAuthelia.Clock.Set(datetime) config := schema.Configuration{} - config.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration - config.Session.Name = "authelia_session" - config.Session.Domain = "example.com" + config.Session.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", + Domain: "example.com", + RememberMe: schema.DefaultSessionConfiguration.RememberMe, + Expiration: schema.DefaultSessionConfiguration.Expiration, + }, + }, + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: "authelia_session", + Domain: "example2.com", + RememberMe: schema.DefaultSessionConfiguration.RememberMe, + Expiration: schema.DefaultSessionConfiguration.Expiration, + }, + }, + } + config.AccessControl.DefaultPolicy = "deny" config.AccessControl.Rules = []schema.ACLRule{{ Domains: []string{"bypass.example.com"}, @@ -114,6 +130,9 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { // Set a cookie to identify this client throughout the test. // request.Request.Header.SetCookie("authelia_session", "client_cookie"). + // Set X-Forwarded-Host for compatibility with multi-root-domain implementation. + request.Request.Header.Set("X-Forwarded-Host", "example.com") + ctx := middlewares.NewAutheliaCtx(request, config, providers) mockAuthelia.Ctx = ctx diff --git a/internal/notification/smtp_notifier.go b/internal/notification/smtp_notifier.go index c212b2c9c..a42fe62f4 100644 --- a/internal/notification/smtp_notifier.go +++ b/internal/notification/smtp_notifier.go @@ -139,8 +139,6 @@ func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject var client *gomail.Client - n.log.Debugf("creating client with %d options: %+v", len(n.opts), n.opts) - if client, err = gomail.NewClient(n.config.Host, n.opts...); err != nil { return fmt.Errorf("notifier: smtp: failed to establish client: %w", err) } diff --git a/internal/server/public_html/index.html b/internal/server/public_html/index.html index e69de29bb..5e384f70f 100644 --- a/internal/server/public_html/index.html +++ b/internal/server/public_html/index.html @@ -0,0 +1,9 @@ +{ + "Base":"{{ .Base }}", + "DuoSelfEnrollment":"{{ .DuoSelfEnrollment }}", + "LogoOverride":"{{ .LogoOverride }}", + "RememberMe":"{{ .RememberMe }}", + "ResetPassword":"{{ .ResetPassword }}", + "ResetPasswordCustomURL":"{{ .ResetPasswordCustomURL }}", + "Theme":"{{ .Theme }}" +} diff --git a/internal/server/template.go b/internal/server/template.go index 2aa24560d..aa24f4b99 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -16,6 +16,7 @@ import ( "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/random" + "github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/templates" ) @@ -57,7 +58,16 @@ func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middle ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce)) } - if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil { + var ( + rememberMe string + provider *session.Session + ) + + if provider, err = ctx.GetSessionProvider(); err == nil { + rememberMe = strconv.FormatBool(!provider.Config.DisableRememberMe) + } + + if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride, rememberMe)); err != nil { ctx.RequestCtx.Error("an error occurred", 503) ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") @@ -190,7 +200,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO opts = &TemplatedFileOptions{ AssetPath: config.Server.AssetPath, DuoSelfEnrollment: strFalse, - RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled), + RememberMe: strconv.FormatBool(!config.Session.DisableRememberMe), ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable), ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(), Theme: config.Theme, @@ -227,7 +237,11 @@ type TemplatedFileOptions struct { } // CommonData returns a TemplatedFileCommonData with the dynamic options. -func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride string) TemplatedFileCommonData { +func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { + if rememberMe != "" { + return options.commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe) + } + return TemplatedFileCommonData{ Base: base, BaseURL: baseURL, @@ -242,6 +256,22 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri } } +// CommonDataWithRememberMe returns a TemplatedFileCommonData with the dynamic options. +func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData { + return TemplatedFileCommonData{ + Base: base, + BaseURL: baseURL, + CSPNonce: nonce, + LogoOverride: logoOverride, + DuoSelfEnrollment: options.DuoSelfEnrollment, + RememberMe: rememberMe, + ResetPassword: options.ResetPassword, + ResetPasswordCustomURL: options.ResetPasswordCustomURL, + Session: options.Session, + Theme: options.Theme, + } +} + // OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options. func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData { return TemplatedFileOpenAPIData{ diff --git a/internal/session/encrypting_serializer.go b/internal/session/encrypting_serializer.go index 3423af958..1fa6ccdb2 100644 --- a/internal/session/encrypting_serializer.go +++ b/internal/session/encrypting_serializer.go @@ -9,6 +9,12 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) +// Serializer is a function that can serialize session information. +type Serializer interface { + Encode(src session.Dict) (data []byte, err error) + Decode(dst *session.Dict, src []byte) (err error) +} + // EncryptingSerializer a serializer encrypting the data with AES-GCM with 256-bit keys. type EncryptingSerializer struct { key [32]byte @@ -21,7 +27,7 @@ func NewEncryptingSerializer(secret string) *EncryptingSerializer { } // Encode encode and encrypt session. -func (e *EncryptingSerializer) Encode(src session.Dict) ([]byte, error) { +func (e *EncryptingSerializer) Encode(src session.Dict) (data []byte, err error) { if len(src.KV) == 0 { return nil, nil } @@ -31,16 +37,15 @@ func (e *EncryptingSerializer) Encode(src session.Dict) ([]byte, error) { return nil, fmt.Errorf("unable to marshal session: %v", err) } - encryptedDst, err := utils.Encrypt(dst, &e.key) - if err != nil { + if data, err = utils.Encrypt(dst, &e.key); err != nil { return nil, fmt.Errorf("unable to encrypt session: %v", err) } - return encryptedDst, nil + return data, nil } // Decode decrypt and decode session. -func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error { +func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) (err error) { if len(src) == 0 { return nil } @@ -49,12 +54,13 @@ func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error { delete(dst.KV, k) } - decryptedSrc, err := utils.Decrypt(src, &e.key) - if err != nil { + var data []byte + + if data, err = utils.Decrypt(src, &e.key); err != nil { return fmt.Errorf("unable to decrypt session: %s", err) } - _, err = dst.UnmarshalMsg(decryptedSrc) + _, err = dst.UnmarshalMsg(data) return err } diff --git a/internal/session/mocks/mock_storer.go b/internal/session/mocks/mock_storer.go deleted file mode 100644 index b710447de..000000000 --- a/internal/session/mocks/mock_storer.go +++ /dev/null @@ -1,208 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/fasthttp/session/v2 (interfaces: Storer) - -// Package mock_session is a generated GoMock package. -package mock_session - -import ( - "reflect" - "time" - - "github.com/fasthttp/session/v2" - "github.com/golang/mock/gomock" -) - -// MockStorer is a mock of Storer interface -type MockStorer struct { - ctrl *gomock.Controller - recorder *MockStorerMockRecorder -} - -// MockStorerMockRecorder is the mock recorder for MockStorer -type MockStorerMockRecorder struct { - mock *MockStorer -} - -// NewMockStorer creates a new mock instance -func NewMockStorer(ctrl *gomock.Controller) *MockStorer { - mock := &MockStorer{ctrl: ctrl} - mock.recorder = &MockStorerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockStorer) EXPECT() *MockStorerMockRecorder { - return m.recorder -} - -// Delete mocks base method -func (m *MockStorer) Delete(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", arg0) -} - -// Delete indicates an expected call of Delete -func (mr *MockStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStorer)(nil).Delete), arg0) -} - -// DeleteBytes mocks base method -func (m *MockStorer) DeleteBytes(arg0 []byte) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteBytes", arg0) -} - -// DeleteBytes indicates an expected call of DeleteBytes -func (mr *MockStorerMockRecorder) DeleteBytes(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBytes", reflect.TypeOf((*MockStorer)(nil).DeleteBytes), arg0) -} - -// Flush mocks base method -func (m *MockStorer) Flush() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Flush") -} - -// Flush indicates an expected call of Flush -func (mr *MockStorerMockRecorder) Flush() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockStorer)(nil).Flush)) -} - -// Get mocks base method -func (m *MockStorer) Get(arg0 string) interface{} { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", arg0) - ret0, _ := ret[0].(interface{}) - return ret0 -} - -// Get indicates an expected call of Get -func (mr *MockStorerMockRecorder) Get(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStorer)(nil).Get), arg0) -} - -// GetAll mocks base method -func (m *MockStorer) GetAll() session.Dict { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAll") - ret0, _ := ret[0].(session.Dict) - return ret0 -} - -// GetAll indicates an expected call of GetAll -func (mr *MockStorerMockRecorder) GetAll() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockStorer)(nil).GetAll)) -} - -// GetBytes mocks base method -func (m *MockStorer) GetBytes(arg0 []byte) interface{} { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBytes", arg0) - ret0, _ := ret[0].(interface{}) - return ret0 -} - -// GetBytes indicates an expected call of GetBytes -func (mr *MockStorerMockRecorder) GetBytes(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBytes", reflect.TypeOf((*MockStorer)(nil).GetBytes), arg0) -} - -// GetExpiration mocks base method -func (m *MockStorer) GetExpiration() time.Duration { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetExpiration") - ret0, _ := ret[0].(time.Duration) - return ret0 -} - -// GetExpiration indicates an expected call of GetExpiration -func (mr *MockStorerMockRecorder) GetExpiration() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiration", reflect.TypeOf((*MockStorer)(nil).GetExpiration)) -} - -// GetSessionID mocks base method -func (m *MockStorer) GetSessionID() []byte { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSessionID") - ret0, _ := ret[0].([]byte) - return ret0 -} - -// GetSessionID indicates an expected call of GetSessionID -func (mr *MockStorerMockRecorder) GetSessionID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionID", reflect.TypeOf((*MockStorer)(nil).GetSessionID)) -} - -// HasExpirationChanged mocks base method -func (m *MockStorer) HasExpirationChanged() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasExpirationChanged") - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasExpirationChanged indicates an expected call of HasExpirationChanged -func (mr *MockStorerMockRecorder) HasExpirationChanged() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasExpirationChanged", reflect.TypeOf((*MockStorer)(nil).HasExpirationChanged)) -} - -// Save mocks base method -func (m *MockStorer) Save() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Save") - ret0, _ := ret[0].(error) - return ret0 -} - -// Save indicates an expected call of Save -func (mr *MockStorerMockRecorder) Save() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockStorer)(nil).Save)) -} - -// Set mocks base method -func (m *MockStorer) Set(arg0 string, arg1 interface{}) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Set", arg0, arg1) -} - -// Set indicates an expected call of Set -func (mr *MockStorerMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockStorer)(nil).Set), arg0, arg1) -} - -// SetBytes mocks base method -func (m *MockStorer) SetBytes(arg0 []byte, arg1 interface{}) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetBytes", arg0, arg1) -} - -// SetBytes indicates an expected call of SetBytes -func (mr *MockStorerMockRecorder) SetBytes(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBytes", reflect.TypeOf((*MockStorer)(nil).SetBytes), arg0, arg1) -} - -// SetExpiration mocks base method -func (m *MockStorer) SetExpiration(arg0 time.Duration) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetExpiration", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetExpiration indicates an expected call of SetExpiration -func (mr *MockStorerMockRecorder) SetExpiration(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExpiration", reflect.TypeOf((*MockStorer)(nil).SetExpiration), arg0) -} diff --git a/internal/session/provider.go b/internal/session/provider.go index 88faef30d..4d6e5091d 100644 --- a/internal/session/provider.go +++ b/internal/session/provider.go @@ -2,158 +2,61 @@ package session import ( "crypto/x509" - "encoding/json" - "time" + "fmt" - fasthttpsession "github.com/fasthttp/session/v2" - "github.com/fasthttp/session/v2/providers/memory" - "github.com/fasthttp/session/v2/providers/redis" - "github.com/valyala/fasthttp" + "github.com/fasthttp/session/v2" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/logging" ) -// Provider a session provider. +// Provider contains a list of domain sessions. type Provider struct { - sessionHolder *fasthttpsession.Session - RememberMe time.Duration - Inactivity time.Duration + sessions map[string]*Session } // NewProvider instantiate a session provider given a configuration. func NewProvider(config schema.SessionConfiguration, certPool *x509.CertPool) *Provider { - c := NewProviderConfig(config, certPool) + log := logging.Logger() - provider := new(Provider) - provider.sessionHolder = fasthttpsession.New(c.config) - - logger := logging.Logger() - - provider.Inactivity, provider.RememberMe = config.Inactivity, config.RememberMeDuration - - var ( - providerImpl fasthttpsession.Provider - err error - ) - - switch { - case c.redisConfig != nil: - providerImpl, err = redis.New(*c.redisConfig) - if err != nil { - logger.Fatal(err) - } - case c.redisSentinelConfig != nil: - providerImpl, err = redis.NewFailoverCluster(*c.redisSentinelConfig) - if err != nil { - logger.Fatal(err) - } - default: - providerImpl, err = memory.New(memory.Config{}) - if err != nil { - logger.Fatal(err) - } + name, p, s, err := NewSessionProvider(config, certPool) + if err != nil { + log.Fatal(err) } - err = provider.sessionHolder.SetProvider(providerImpl) - if err != nil { - logger.Fatal(err) + provider := &Provider{ + sessions: map[string]*Session{}, + } + + var ( + holder *session.Session + ) + + for _, dconfig := range config.Cookies { + if _, holder, err = NewProviderConfigAndSession(dconfig, name, s, p); err != nil { + log.Fatal(err) + } + + provider.sessions[dconfig.Domain] = &Session{ + Config: dconfig, + sessionHolder: holder, + } } return provider } -// GetSession return the user session from a request. -func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) { - store, err := p.sessionHolder.Get(ctx) - - if err != nil { - return NewDefaultUserSession(), err +// Get returns session information for specified domain. +func (p *Provider) Get(domain string) (*Session, error) { + if domain == "" { + return nil, fmt.Errorf("can not get session from an undefined domain") } - userSessionJSON, ok := store.Get(userSessionStorerKey).([]byte) + s, found := p.sessions[domain] - // If userSession is not yet defined we create the new session with default values - // and save it in the store. - if !ok { - userSession := NewDefaultUserSession() - - store.Set(userSessionStorerKey, userSession) - - return userSession, nil + if !found { + return nil, fmt.Errorf("no session found for domain '%s'", domain) } - var userSession UserSession - err = json.Unmarshal(userSessionJSON, &userSession) - - if err != nil { - return NewDefaultUserSession(), err - } - - return userSession, nil -} - -// SaveSession save the user session. -func (p *Provider) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) error { - store, err := p.sessionHolder.Get(ctx) - - if err != nil { - return err - } - - userSessionJSON, err := json.Marshal(userSession) - - if err != nil { - return err - } - - store.Set(userSessionStorerKey, userSessionJSON) - - err = p.sessionHolder.Save(ctx, store) - - if err != nil { - return err - } - - return nil -} - -// RegenerateSession regenerate a session ID. -func (p *Provider) RegenerateSession(ctx *fasthttp.RequestCtx) error { - err := p.sessionHolder.Regenerate(ctx) - - return err -} - -// DestroySession destroy a session ID and delete the cookie. -func (p *Provider) DestroySession(ctx *fasthttp.RequestCtx) error { - return p.sessionHolder.Destroy(ctx) -} - -// UpdateExpiration update the expiration of the cookie and session. -func (p *Provider) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) error { - store, err := p.sessionHolder.Get(ctx) - - if err != nil { - return err - } - - err = store.SetExpiration(expiration) - - if err != nil { - return err - } - - return p.sessionHolder.Save(ctx, store) -} - -// GetExpiration get the expiration of the current session. -func (p *Provider) GetExpiration(ctx *fasthttp.RequestCtx) (time.Duration, error) { - store, err := p.sessionHolder.Get(ctx) - - if err != nil { - return time.Duration(0), err - } - - return store.GetExpiration(), nil + return s, nil } diff --git a/internal/session/provider_config.go b/internal/session/provider_config.go index c9e694a28..0336a703e 100644 --- a/internal/session/provider_config.go +++ b/internal/session/provider_config.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/fasthttp/session/v2" + "github.com/fasthttp/session/v2/providers/memory" "github.com/fasthttp/session/v2/providers/redis" "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" @@ -18,7 +19,7 @@ import ( ) // NewProviderConfig creates a configuration for creating the session provider. -func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPool) ProviderConfig { +func NewProviderConfig(config schema.SessionCookieConfiguration, providerName string, serializer Serializer) ProviderConfig { c := session.NewDefaultConfig() c.SessionIDGeneratorFunc = func() []byte { @@ -61,16 +62,42 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo return true } - var redisConfig *redis.Config + if serializer != nil { + c.EncodeFunc = serializer.Encode + c.DecodeFunc = serializer.Decode + } - var redisSentinelConfig *redis.FailoverConfig + return ProviderConfig{ + c, + providerName, + } +} - var providerName string +func NewProviderSession(pconfig ProviderConfig, provider session.Provider) (p *session.Session, err error) { + p = session.New(pconfig.config) + if err = p.SetProvider(provider); err != nil { + return nil, err + } + + return p, nil +} + +func NewProviderConfigAndSession(config schema.SessionCookieConfiguration, providerName string, serializer Serializer, provider session.Provider) (c ProviderConfig, p *session.Session, err error) { + c = NewProviderConfig(config, providerName, serializer) + + if p, err = NewProviderSession(c, provider); err != nil { + return c, nil, err + } + + return c, p, nil +} + +func NewSessionProvider(config schema.SessionConfiguration, certPool *x509.CertPool) (name string, provider session.Provider, serializer Serializer, err error) { // If redis configuration is provided, then use the redis provider. switch { case config.Redis != nil: - serializer := NewEncryptingSerializer(config.Secret) + serializer = NewEncryptingSerializer(config.Secret) var tlsConfig *tls.Config @@ -92,8 +119,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo } } - providerName = "redis-sentinel" - redisSentinelConfig = &redis.FailoverConfig{ + name = "redis-sentinel" + + provider, err = redis.NewFailoverCluster(redis.FailoverConfig{ Logger: logging.LoggerCtxPrintf(logrus.TraceLevel), MasterName: config.Redis.HighAvailability.SentinelName, SentinelAddrs: addrs, @@ -109,9 +137,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo IdleTimeout: 300, TLSConfig: tlsConfig, KeyPrefix: "authelia-session", - } + }) } else { - providerName = "redis" + name = "redis" network := "tcp" var addr string @@ -123,7 +151,7 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo addr = fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port) } - redisConfig = &redis.Config{ + provider, err = redis.New(redis.Config{ Logger: logging.LoggerCtxPrintf(logrus.TraceLevel), Network: network, Addr: addr, @@ -135,19 +163,12 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo IdleTimeout: 300, TLSConfig: tlsConfig, KeyPrefix: "authelia-session", - } + }) } - - c.EncodeFunc = serializer.Encode - c.DecodeFunc = serializer.Decode default: - providerName = "memory" + name = "memory" + provider, err = memory.New(memory.Config{}) } - return ProviderConfig{ - c, - redisConfig, - redisSentinelConfig, - providerName, - } + return name, provider, serializer, err } diff --git a/internal/session/provider_config_test.go b/internal/session/provider_config_test.go deleted file mode 100644 index 6e3d5f110..000000000 --- a/internal/session/provider_config_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package session - -import ( - "crypto/sha256" - "crypto/tls" - "testing" - "time" - - "github.com/fasthttp/session/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" - - "github.com/authelia/authelia/v4/internal/configuration/schema" - "github.com/authelia/authelia/v4/internal/utils" -) - -func TestShouldCreateInMemorySessionProvider(t *testing.T) { - // The redis configuration is not provided so we create a in-memory provider. - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - providerConfig := NewProviderConfig(configuration, nil) - - assert.Equal(t, "my_session", providerConfig.config.CookieName) - assert.Equal(t, testDomain, providerConfig.config.Domain) - assert.Equal(t, true, providerConfig.config.Secure) - assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration) - assert.True(t, providerConfig.config.IsSecureFunc(nil)) - assert.Equal(t, "memory", providerConfig.providerName) -} - -func TestShouldCreateRedisSessionProviderTLS(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "redis.example.com", - Port: 6379, - Password: "pass", - TLS: &schema.TLSConfig{ - ServerName: "redis.fqdn.example.com", - MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13}, - }, - } - providerConfig := NewProviderConfig(configuration, nil) - - assert.Nil(t, providerConfig.redisSentinelConfig) - assert.Equal(t, "my_session", providerConfig.config.CookieName) - assert.Equal(t, testDomain, providerConfig.config.Domain) - assert.Equal(t, true, providerConfig.config.Secure) - assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration) - assert.True(t, providerConfig.config.IsSecureFunc(nil)) - - assert.Equal(t, "redis", providerConfig.providerName) - - pConfig := providerConfig.redisConfig - assert.Equal(t, "redis.example.com:6379", pConfig.Addr) - assert.Equal(t, "pass", pConfig.Password) - // DbNumber is the fasthttp/session property for the Redis DB Index. - assert.Equal(t, 0, pConfig.DB) - assert.Equal(t, 0, pConfig.PoolSize) - assert.Equal(t, 0, pConfig.MinIdleConns) - - require.NotNil(t, pConfig.TLSConfig) - require.Equal(t, uint16(tls.VersionTLS13), pConfig.TLSConfig.MinVersion) - require.Equal(t, "redis.fqdn.example.com", pConfig.TLSConfig.ServerName) - require.False(t, pConfig.TLSConfig.InsecureSkipVerify) -} - -func TestShouldCreateRedisSessionProvider(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "redis.example.com", - Port: 6379, - Password: "pass", - } - providerConfig := NewProviderConfig(configuration, nil) - - assert.Nil(t, providerConfig.redisSentinelConfig) - assert.Equal(t, "my_session", providerConfig.config.CookieName) - assert.Equal(t, testDomain, providerConfig.config.Domain) - assert.Equal(t, true, providerConfig.config.Secure) - assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration) - assert.True(t, providerConfig.config.IsSecureFunc(nil)) - - assert.Equal(t, "redis", providerConfig.providerName) - - pConfig := providerConfig.redisConfig - assert.Equal(t, "redis.example.com:6379", pConfig.Addr) - assert.Equal(t, "pass", pConfig.Password) - // DbNumber is the fasthttp/session property for the Redis DB Index. - assert.Equal(t, 0, pConfig.DB) - assert.Equal(t, 0, pConfig.PoolSize) - assert.Equal(t, 0, pConfig.MinIdleConns) - - assert.Nil(t, pConfig.TLSConfig) -} - -func TestShouldCreateRedisSentinelSessionProviderWithoutDuplicateHosts(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "REDIS.example.com", - Port: 26379, - Password: "pass", - MaximumActiveConnections: 8, - MinimumIdleConnections: 2, - HighAvailability: &schema.RedisHighAvailabilityConfiguration{ - SentinelName: "mysent", - SentinelPassword: "mypass", - Nodes: []schema.RedisNode{ - { - Host: "redis2.example.com", - Port: 26379, - }, - { - Host: "redis.example.com", - Port: 26379, - }, - }, - }, - } - - providerConfig := NewProviderConfig(configuration, nil) - - assert.Len(t, providerConfig.redisSentinelConfig.SentinelAddrs, 2) - assert.Equal(t, providerConfig.redisSentinelConfig.SentinelAddrs[0], "redis.example.com:26379") - assert.Equal(t, providerConfig.redisSentinelConfig.SentinelAddrs[1], "redis2.example.com:26379") -} - -func TestShouldCreateRedisSentinelSessionProvider(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "redis.example.com", - Port: 26379, - Password: "pass", - MaximumActiveConnections: 8, - MinimumIdleConnections: 2, - HighAvailability: &schema.RedisHighAvailabilityConfiguration{ - SentinelName: "mysent", - SentinelPassword: "mypass", - Nodes: []schema.RedisNode{ - { - Host: "redis2.example.com", - Port: 26379, - }, - }, - }, - } - providerConfig := NewProviderConfig(configuration, nil) - - assert.Nil(t, providerConfig.redisConfig) - assert.Equal(t, "my_session", providerConfig.config.CookieName) - assert.Equal(t, testDomain, providerConfig.config.Domain) - assert.Equal(t, true, providerConfig.config.Secure) - assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration) - assert.True(t, providerConfig.config.IsSecureFunc(nil)) - - assert.Equal(t, "redis-sentinel", providerConfig.providerName) - - pConfig := providerConfig.redisSentinelConfig - assert.Equal(t, "redis.example.com:26379", pConfig.SentinelAddrs[0]) - assert.Equal(t, "redis2.example.com:26379", pConfig.SentinelAddrs[1]) - assert.Equal(t, "pass", pConfig.Password) - assert.Equal(t, "mysent", pConfig.MasterName) - assert.Equal(t, "mypass", pConfig.SentinelPassword) - assert.False(t, pConfig.RouteRandomly) - assert.False(t, pConfig.RouteByLatency) - assert.Equal(t, 8, pConfig.PoolSize) - assert.Equal(t, 2, pConfig.MinIdleConns) - - // DbNumber is the fasthttp/session property for the Redis DB Index. - assert.Equal(t, 0, pConfig.DB) - assert.Nil(t, pConfig.TLSConfig) -} - -func TestShouldSetCookieSameSite(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - - configValueExpectedValue := map[string]fasthttp.CookieSameSite{ - "": fasthttp.CookieSameSiteLaxMode, - "lax": fasthttp.CookieSameSiteLaxMode, - "strict": fasthttp.CookieSameSiteStrictMode, - "none": fasthttp.CookieSameSiteNoneMode, - "invalid": fasthttp.CookieSameSiteLaxMode, - } - - for configValue, expectedValue := range configValueExpectedValue { - configuration.SameSite = configValue - providerConfig := NewProviderConfig(configuration, nil) - - assert.Equal(t, expectedValue, providerConfig.config.CookieSameSite) - } -} - -func TestShouldCreateRedisSessionProviderWithUnixSocket(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "/var/run/redis/redis.sock", - Port: 0, - Password: "pass", - } - - providerConfig := NewProviderConfig(configuration, nil) - - assert.Nil(t, providerConfig.redisSentinelConfig) - - assert.Equal(t, "my_session", providerConfig.config.CookieName) - assert.Equal(t, testDomain, providerConfig.config.Domain) - assert.Equal(t, true, providerConfig.config.Secure) - assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration) - assert.True(t, providerConfig.config.IsSecureFunc(nil)) - - assert.Equal(t, "redis", providerConfig.providerName) - - pConfig := providerConfig.redisConfig - assert.Equal(t, "/var/run/redis/redis.sock", pConfig.Addr) - assert.Equal(t, "pass", pConfig.Password) - // DbNumber is the fasthttp/session property for the Redis DB Index. - assert.Equal(t, 0, pConfig.DB) - assert.Nil(t, pConfig.TLSConfig) -} - -func TestShouldSetDbNumber(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "redis.example.com", - Port: 6379, - Password: "pass", - DatabaseIndex: 5, - } - - providerConfig := NewProviderConfig(configuration, nil) - - assert.Nil(t, providerConfig.redisSentinelConfig) - - assert.Equal(t, "redis", providerConfig.providerName) - pConfig := providerConfig.redisConfig - // DbNumber is the fasthttp/session property for the Redis DB Index. - assert.Equal(t, 5, pConfig.DB) -} - -func TestShouldUseEncryptingSerializerWithRedis(t *testing.T) { - configuration := schema.SessionConfiguration{} - configuration.Secret = "abc" - configuration.Redis = &schema.RedisSessionConfiguration{ - Host: "redis.example.com", - Port: 6379, - Password: "pass", - DatabaseIndex: 5, - } - providerConfig := NewProviderConfig(configuration, nil) - - payload := session.Dict{KV: map[string]interface{}{"key": "value"}} - - encoded, err := providerConfig.config.EncodeFunc(payload) - require.NoError(t, err) - - // Now we try to decrypt what has been serialized. - key := sha256.Sum256([]byte("abc")) - decrypted, err := utils.Decrypt(encoded, &key) - require.NoError(t, err) - - decoded := session.Dict{} - _, _ = decoded.UnmarshalMsg(decrypted) - assert.Equal(t, "value", decoded.KV["key"]) -} diff --git a/internal/session/provider_test.go b/internal/session/provider_test.go index 5deb312d9..81eab3cff 100644 --- a/internal/session/provider_test.go +++ b/internal/session/provider_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/authentication" @@ -14,16 +13,31 @@ import ( "github.com/authelia/authelia/v4/internal/oidc" ) +func newTestSession() (*Session, error) { + config := schema.SessionConfiguration{} + config.Cookies = []schema.SessionCookieConfiguration{ + { + SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{ + Name: testName, + Domain: testDomain, + Expiration: testExpiration, + }, + }, + } + + provider := NewProvider(config, nil) + + return provider.Get(testDomain) +} + func TestShouldInitializerSession(t *testing.T) { ctx := &fasthttp.RequestCtx{} - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration - provider := NewProvider(configuration, nil) + provider, err := newTestSession() + assert.NoError(t, err) + session, err := provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, NewDefaultUserSession(), session) } @@ -31,22 +45,19 @@ func TestShouldInitializerSession(t *testing.T) { func TestShouldUpdateSession(t *testing.T) { ctx := &fasthttp.RequestCtx{} - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration + provider, err := newTestSession() + assert.NoError(t, err) - provider := NewProvider(configuration, nil) session, _ := provider.GetSession(ctx) session.Username = testUsername session.AuthenticationLevel = authentication.TwoFactor - err := provider.SaveSession(ctx, session) - require.NoError(t, err) + err = provider.SaveSession(ctx, session) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, UserSession{ Username: testUsername, @@ -56,26 +67,23 @@ func TestShouldUpdateSession(t *testing.T) { func TestShouldSetSessionAuthenticationLevels(t *testing.T) { ctx := &fasthttp.RequestCtx{} - configuration := schema.SessionConfiguration{} timeOneFactor := time.Unix(1625048140, 0) timeTwoFactor := time.Unix(1625048150, 0) timeZeroFactor := time.Unix(0, 0) - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration + provider, err := newTestSession() + assert.NoError(t, err) - provider := NewProvider(configuration, nil) session, _ := provider.GetSession(ctx) session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false) - err := provider.SaveSession(ctx, session) - require.NoError(t, err) + err = provider.SaveSession(ctx, session) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) authAt, err := session.AuthenticatedTime(authorization.OneFactor) assert.NoError(t, err) @@ -100,10 +108,10 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) { session.SetTwoFactorDuo(timeTwoFactor) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, UserSession{ Username: testUsername, @@ -129,26 +137,23 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) { func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { ctx := &fasthttp.RequestCtx{} - configuration := schema.SessionConfiguration{} timeOneFactor := time.Unix(1625048140, 0) timeTwoFactor := time.Unix(1625048150, 0) timeZeroFactor := time.Unix(0, 0) - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration + provider, err := newTestSession() + assert.NoError(t, err) - provider := NewProvider(configuration, nil) session, _ := provider.GetSession(ctx) session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false) - err := provider.SaveSession(ctx, session) - require.NoError(t, err) + err = provider.SaveSession(ctx, session) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) authAt, err := session.AuthenticatedTime(authorization.OneFactor) assert.NoError(t, err) @@ -173,10 +178,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, false, false) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs) assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication()) @@ -196,10 +201,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, false, false) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, @@ -208,10 +213,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, false, false) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, @@ -220,10 +225,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, true, false) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, @@ -232,10 +237,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, true, false) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, @@ -244,10 +249,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, false, true) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, @@ -256,10 +261,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorWebauthn(timeTwoFactor, false, true) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, @@ -268,10 +273,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorTOTP(timeTwoFactor) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, @@ -280,10 +285,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { session.SetTwoFactorTOTP(timeTwoFactor) err = provider.SaveSession(ctx, session) - require.NoError(t, err) + assert.NoError(t, err) session, err = provider.GetSession(ctx) - require.NoError(t, err) + assert.NoError(t, err) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, @@ -292,31 +297,28 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { func TestShouldDestroySessionAndWipeSessionData(t *testing.T) { ctx := &fasthttp.RequestCtx{} - configuration := schema.SessionConfiguration{} - configuration.Domain = testDomain - configuration.Name = testName - configuration.Expiration = testExpiration + domainSession, err := newTestSession() + assert.NoError(t, err) - provider := NewProvider(configuration, nil) - session, err := provider.GetSession(ctx) - require.NoError(t, err) + session, err := domainSession.GetSession(ctx) + assert.NoError(t, err) session.Username = testUsername session.AuthenticationLevel = authentication.TwoFactor - err = provider.SaveSession(ctx, session) - require.NoError(t, err) + err = domainSession.SaveSession(ctx, session) + assert.NoError(t, err) - newUserSession, err := provider.GetSession(ctx) - require.NoError(t, err) + newUserSession, err := domainSession.GetSession(ctx) + assert.NoError(t, err) assert.Equal(t, testUsername, newUserSession.Username) assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel) - err = provider.DestroySession(ctx) - require.NoError(t, err) + err = domainSession.DestroySession(ctx) + assert.NoError(t, err) - newUserSession, err = provider.GetSession(ctx) - require.NoError(t, err) + newUserSession, err = domainSession.GetSession(ctx) + assert.NoError(t, err) assert.Equal(t, "", newUserSession.Username) assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel) } diff --git a/internal/session/session.go b/internal/session/session.go new file mode 100644 index 000000000..89c32830e --- /dev/null +++ b/internal/session/session.go @@ -0,0 +1,113 @@ +package session + +import ( + "encoding/json" + "time" + + fasthttpsession "github.com/fasthttp/session/v2" + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/v4/internal/configuration/schema" +) + +// Session a session provider. +type Session struct { + Config schema.SessionCookieConfiguration + + sessionHolder *fasthttpsession.Session +} + +// GetSession return the user session from a request. +func (p *Session) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) { + store, err := p.sessionHolder.Get(ctx) + + if err != nil { + return NewDefaultUserSession(), err + } + + userSessionJSON, ok := store.Get(userSessionStorerKey).([]byte) + + // If userSession is not yet defined we create the new session with default values + // and save it in the store. + if !ok { + userSession := NewDefaultUserSession() + + store.Set(userSessionStorerKey, userSession) + + return userSession, nil + } + + var userSession UserSession + err = json.Unmarshal(userSessionJSON, &userSession) + + if err != nil { + return NewDefaultUserSession(), err + } + + return userSession, nil +} + +// SaveSession save the user session. +func (p *Session) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) error { + store, err := p.sessionHolder.Get(ctx) + + if err != nil { + return err + } + + userSessionJSON, err := json.Marshal(userSession) + + if err != nil { + return err + } + + store.Set(userSessionStorerKey, userSessionJSON) + + err = p.sessionHolder.Save(ctx, store) + + if err != nil { + return err + } + + return nil +} + +// RegenerateSession regenerate a session ID. +func (p *Session) RegenerateSession(ctx *fasthttp.RequestCtx) error { + err := p.sessionHolder.Regenerate(ctx) + + return err +} + +// DestroySession destroy a session ID and delete the cookie. +func (p *Session) DestroySession(ctx *fasthttp.RequestCtx) error { + return p.sessionHolder.Destroy(ctx) +} + +// UpdateExpiration update the expiration of the cookie and session. +func (p *Session) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) error { + store, err := p.sessionHolder.Get(ctx) + + if err != nil { + return err + } + + err = store.SetExpiration(expiration) + + if err != nil { + return err + } + + return p.sessionHolder.Save(ctx, store) +} + +// GetExpiration get the expiration of the current session. +func (p *Session) GetExpiration(ctx *fasthttp.RequestCtx) (time.Duration, error) { + store, err := p.sessionHolder.Get(ctx) + + if err != nil { + return time.Duration(0), err + } + + return store.GetExpiration(), nil +} diff --git a/internal/session/types.go b/internal/session/types.go index a3adbb89b..f256da2cb 100644 --- a/internal/session/types.go +++ b/internal/session/types.go @@ -3,8 +3,7 @@ package session import ( "time" - session "github.com/fasthttp/session/v2" - "github.com/fasthttp/session/v2/providers/redis" + "github.com/fasthttp/session/v2" "github.com/go-webauthn/webauthn/webauthn" "github.com/authelia/authelia/v4/internal/authentication" @@ -13,10 +12,8 @@ import ( // ProviderConfig is the configuration used to create the session provider. type ProviderConfig struct { - config session.Config - redisConfig *redis.Config - redisSentinelConfig *redis.FailoverConfig - providerName string + config session.Config + providerName string } // UserSession is the structure representing the session of a user. diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml index a1adb91cd..31f09189f 100644 --- a/internal/suites/ActiveDirectory/configuration.yml +++ b/internal/suites/ActiveDirectory/configuration.yml @@ -34,7 +34,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml index 25a4c5c8b..e2b592a5f 100644 --- a/internal/suites/BypassAll/configuration.yml +++ b/internal/suites/BypassAll/configuration.yml @@ -23,7 +23,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/CLI/configuration.yml b/internal/suites/CLI/configuration.yml index 9a978c5ae..99a1aee7d 100644 --- a/internal/suites/CLI/configuration.yml +++ b/internal/suites/CLI/configuration.yml @@ -20,10 +20,13 @@ authentication_backend: session: secret: unsecure_session_secret - domain: example.com - expiration: 3600 # 1 hour - inactivity: 300 # 5 minutes - remember_me_duration: 1y + cookies: + - name: 'authelia_session' + domain: 'example.com' + authelia_url: 'https://login.example.com' + expiration: 3600 # 1 hour + inactivity: 300 # 5 minutes + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Caddy/configuration.yml b/internal/suites/Caddy/configuration.yml index 31c5d75ba..4ce6a5b4d 100644 --- a/internal/suites/Caddy/configuration.yml +++ b/internal/suites/Caddy/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml index 5ac4aaa5b..d37132c67 100644 --- a/internal/suites/Docker/configuration.yml +++ b/internal/suites/Docker/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml index 368dca94f..cb8caaec0 100644 --- a/internal/suites/DuoPush/configuration.yml +++ b/internal/suites/DuoPush/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/internal/suites/Envoy/configuration.yml b/internal/suites/Envoy/configuration.yml index 7a5f55448..e1a05f80d 100644 --- a/internal/suites/Envoy/configuration.yml +++ b/internal/suites/Envoy/configuration.yml @@ -21,10 +21,12 @@ authentication_backend: session: secret: unsecure_session_secret - domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y + cookies: + - name: 'authelia_session' + domain: 'example.com' storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/HAProxy/configuration.yml b/internal/suites/HAProxy/configuration.yml index 193cae03d..1de216395 100644 --- a/internal/suites/HAProxy/configuration.yml +++ b/internal/suites/HAProxy/configuration.yml @@ -23,7 +23,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml index 2b0cc5987..530b9bdf3 100644 --- a/internal/suites/HighAvailability/configuration.yml +++ b/internal/suites/HighAvailability/configuration.yml @@ -102,7 +102,7 @@ session: - host: redis-sentinel-2 port: 26379 - remember_me_duration: 1y + remember_me: 1y regulation: max_retries: 3 diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index 4bcd9a307..f69a46c7c 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -38,7 +38,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/MariaDB/configuration.yml b/internal/suites/MariaDB/configuration.yml index 0c406aa1f..3228275b3 100644 --- a/internal/suites/MariaDB/configuration.yml +++ b/internal/suites/MariaDB/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/internal/suites/MultiCookieDomain/configuration.yml b/internal/suites/MultiCookieDomain/configuration.yml new file mode 100644 index 000000000..30e194678 --- /dev/null +++ b/internal/suites/MultiCookieDomain/configuration.yml @@ -0,0 +1,219 @@ +--- +############################################################### +# Authelia minimal configuration # +############################################################### + +jwt_secret: unsecure_secret +theme: auto + +server: + port: 9091 + tls: + certificate: /config/ssl/cert.pem + key: /config/ssl/key.pem + +telemetry: + metrics: + enabled: true + address: tcp://0.0.0.0:9959 + +log: + level: debug + +authentication_backend: + file: + path: /config/users.yml + +session: + secret: unsecure_session_secret + expiration: 3600 + inactivity: 300 + remember_me: 1y + cookies: + - name: 'authelia_session' + domain: 'example.com' + - name: 'example2_session' + domain: 'example2.com' + authelia_url: 'https://login.example2.com' + remember_me: -1 + - name: 'authelia_session' + domain: 'example3.com' + authelia_url: 'https://login.example3.com' + +storage: + encryption_key: a_not_so_secure_encryption_key + local: + path: /config/db.sqlite + +totp: + issuer: example.com + +access_control: + default_policy: deny + + rules: + # First cookie domain + - domain: singlefactor.example.com + policy: one_factor + + - domain: public.example.com + policy: bypass + + - domain: secure.example.com + policy: bypass + methods: + - OPTIONS + + - domain: secure.example.com + policy: two_factor + + - domain: "*.example.com" + subject: "group:admins" + policy: two_factor + + - domain: dev.example.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + - domain: dev.example.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + - domain: "*.mail.example.com" + subject: "user:bob" + policy: two_factor + + - domain: dev.example.com + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + + # Second cookie domain + - domain: singlefactor.example2.com + policy: one_factor + + - domain: public.example2.com + policy: bypass + + - domain: secure.example2.com + policy: bypass + methods: + - OPTIONS + + - domain: secure.example2.com + policy: two_factor + + - domain: "*.example2.com" + subject: "group:admins" + policy: two_factor + + - domain: dev.example2.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + - domain: dev.example2.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + - domain: "*.mail.example2.com" + subject: "user:bob" + policy: two_factor + + - domain: dev.example2.com + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + + # Third cookie domain + - domain: singlefactor.example3.com + policy: one_factor + + - domain: public.example3.com + policy: bypass + + - domain: secure.example3.com + policy: bypass + methods: + - OPTIONS + + - domain: secure.example3.com + policy: two_factor + + - domain: "*.example3.com" + subject: "group:admins" + policy: two_factor + + - domain: dev.example3.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + - domain: dev.example3.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + - domain: "*.mail.example3.com" + subject: "user:bob" + policy: two_factor + + - domain: dev.example3.com + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + + +regulation: + # Set it to 0 to disable max_retries. + max_retries: 3 + # The user is banned if the authentication failed `max_retries` times in a `find_time` seconds window. + find_time: 300 + # The length of time before a banned user can login again. + ban_time: 900 + +notifier: + smtp: + host: smtp + port: 1025 + sender: admin@example.com + disable_require_tls: true +ntp: + ## NTP server address + address: "time.cloudflare.com:123" + ## ntp version + version: 4 + ## "maximum desynchronization" is the allowed offset time between the host and the ntp server + max_desync: 3s + ## You can enable or disable the NTP synchronization check on startup + disable_startup_check: false + +password_policy: + standard: + # Enables standard password Policy + enabled: false + min_length: 8 + max_length: 0 + require_uppercase: true + require_lowercase: true + require_number: true + require_special: true + zxcvbn: + ## zxcvbn: uses zxcvbn for password strength checking (see: https://github.com/dropbox/zxcvbn) + ## Note that the zxcvbn option does not prohibit the user from using a weak password, + ## it only offers feedback about the strength of the password they are entering. + ## if you need to enforce password rules, you should use `mode=classic` + enabled: false +... diff --git a/internal/suites/MultiCookieDomain/docker-compose.yml b/internal/suites/MultiCookieDomain/docker-compose.yml new file mode 100644 index 000000000..1a2f0122b --- /dev/null +++ b/internal/suites/MultiCookieDomain/docker-compose.yml @@ -0,0 +1,9 @@ +--- +version: '3' +services: + authelia-backend: + volumes: + - './MultiCookieDomain/configuration.yml:/config/configuration.yml:ro' + - './MultiCookieDomain/users.yml:/config/users.yml' + - './common/ssl:/config/ssl:ro' +... diff --git a/internal/suites/MultiCookieDomain/users.yml b/internal/suites/MultiCookieDomain/users.yml new file mode 100644 index 000000000..a52978b20 --- /dev/null +++ b/internal/suites/MultiCookieDomain/users.yml @@ -0,0 +1,35 @@ +--- +############################################################### +# Users Database # +############################################################### + +# This file can be used if you do not have an LDAP set up. + +# List of users +users: + john: + displayname: "John Doe" + password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + displayname: "Harry Potter" + password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length + email: harry.potter@authelia.com + groups: [] + + bob: + displayname: "Bob Dylan" + password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length + email: bob.dylan@authelia.com + groups: + - dev + + james: + displayname: "James Dean" + password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length + email: james.dean@authelia.com +... diff --git a/internal/suites/MySQL/configuration.yml b/internal/suites/MySQL/configuration.yml index f57595375..cf8742d0b 100644 --- a/internal/suites/MySQL/configuration.yml +++ b/internal/suites/MySQL/configuration.yml @@ -25,7 +25,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/internal/suites/NetworkACL/configuration.yml b/internal/suites/NetworkACL/configuration.yml index a4a6103d0..5bdf2fa87 100644 --- a/internal/suites/NetworkACL/configuration.yml +++ b/internal/suites/NetworkACL/configuration.yml @@ -23,7 +23,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/internal/suites/OIDC/configuration.yml b/internal/suites/OIDC/configuration.yml index 9e0c5f383..543c6da38 100644 --- a/internal/suites/OIDC/configuration.yml +++ b/internal/suites/OIDC/configuration.yml @@ -16,10 +16,13 @@ authentication_backend: session: secret: unsecure_session_secret - domain: example.com - expiration: 3600 # 1 hour - inactivity: 300 # 5 minutes - remember_me_duration: 1y + + cookies: + - domain: example.com + expiration: 3600 # 1 hour + inactivity: 300 # 5 minutes + remember_me: 1y + # We use redis here to keep the users authenticated when Authelia restarts # It eases development. redis: diff --git a/internal/suites/OIDCTraefik/configuration.yml b/internal/suites/OIDCTraefik/configuration.yml index 1766da293..367dc4e2b 100644 --- a/internal/suites/OIDCTraefik/configuration.yml +++ b/internal/suites/OIDCTraefik/configuration.yml @@ -19,7 +19,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # We use redis here to keep the users authenticated when Authelia restarts # It eases development. redis: diff --git a/internal/suites/OneFactorOnly/configuration.yml b/internal/suites/OneFactorOnly/configuration.yml index a2d44f649..203a803bd 100644 --- a/internal/suites/OneFactorOnly/configuration.yml +++ b/internal/suites/OneFactorOnly/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/PathPrefix/configuration.yml b/internal/suites/PathPrefix/configuration.yml index b2865affc..323187c5e 100644 --- a/internal/suites/PathPrefix/configuration.yml +++ b/internal/suites/PathPrefix/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml index a95ffa96c..958bf763a 100644 --- a/internal/suites/Postgres/configuration.yml +++ b/internal/suites/Postgres/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y # 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 0ac5fe3fe..e73648a65 100644 --- a/internal/suites/ShortTimeouts/configuration.yml +++ b/internal/suites/ShortTimeouts/configuration.yml @@ -21,10 +21,12 @@ authentication_backend: session: secret: unsecure_session_secret - domain: example.com - inactivity: 5 - expiration: 8 - remember_me_duration: 1y + cookies: + - name: authelia_session + domain: example.com + inactivity: 5 + expiration: 8 + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml index 5a959c9b3..a86b20807 100644 --- a/internal/suites/Standalone/configuration.yml +++ b/internal/suites/Standalone/configuration.yml @@ -25,9 +25,9 @@ authentication_backend: session: domain: example.com - expiration: 3600 # 1 hour - inactivity: 300 # 5 minutes - remember_me_duration: 1y + expiration: 3600 + inactivity: 300 + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key @@ -81,6 +81,7 @@ access_control: subject: "user:bob" policy: two_factor + regulation: # Set it to 0 to disable max_retries. max_retries: 3 diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml index 31c5d75ba..4ce6a5b4d 100644 --- a/internal/suites/Traefik/configuration.yml +++ b/internal/suites/Traefik/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y storage: encryption_key: a_not_so_secure_encryption_key diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml index 3192c9bec..8442be3cb 100644 --- a/internal/suites/Traefik2/configuration.yml +++ b/internal/suites/Traefik2/configuration.yml @@ -24,7 +24,7 @@ session: domain: example.com expiration: 3600 # 1 hour inactivity: 300 # 5 minutes - remember_me_duration: 1y + remember_me: 1y redis: host: redis port: 6379 diff --git a/internal/suites/action_login.go b/internal/suites/action_login.go index 9eaf193a7..d44aec6e4 100644 --- a/internal/suites/action_login.go +++ b/internal/suites/action_login.go @@ -45,14 +45,14 @@ click: } // Login 1FA. -func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, targetURL string) { - rs.doVisitLoginPage(t, page, targetURL) +func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, domain string, targetURL string) { + rs.doVisitLoginPage(t, page, domain, targetURL) rs.doFillLoginPageAndClick(t, page, username, password, keepMeLoggedIn) } // Login 1FA and 2FA subsequently (must already be registered). func (rs *RodSession) doLoginTwoFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) { - rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, targetURL) + rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, BaseDomain, targetURL) rs.verifyIsSecondFactorPage(t, page) rs.doValidateTOTP(t, page, otpSecret) // timeout when targetURL is not defined to prevent a show stopping redirect when visiting a protected domain. @@ -63,9 +63,9 @@ func (rs *RodSession) doLoginTwoFactor(t *testing.T, page *rod.Page, username, p // Login 1FA and register 2FA. func (rs *RodSession) doLoginAndRegisterTOTP(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool) string { - rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, "") + rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, BaseDomain, "") secret := rs.doRegisterTOTP(t, page) - rs.doVisit(t, page, GetLoginBaseURL()) + rs.doVisit(t, page, GetLoginBaseURL(BaseDomain)) rs.verifyIsSecondFactorPage(t, page) return secret diff --git a/internal/suites/action_logout.go b/internal/suites/action_logout.go index c8c27afcc..56e8c6706 100644 --- a/internal/suites/action_logout.go +++ b/internal/suites/action_logout.go @@ -9,12 +9,12 @@ import ( ) func (rs *RodSession) doLogout(t *testing.T, page *rod.Page) { - rs.doVisit(t, page, fmt.Sprintf("%s%s", GetLoginBaseURL(), "/logout")) + rs.doVisit(t, page, fmt.Sprintf("%s%s", GetLoginBaseURL(BaseDomain), "/logout")) rs.verifyIsFirstFactorPage(t, page) } func (rs *RodSession) doLogoutWithRedirect(t *testing.T, page *rod.Page, targetURL string, firstFactor bool) { - rs.doVisit(t, page, fmt.Sprintf("%s%s%s", GetLoginBaseURL(), "/logout?rd=", url.QueryEscape(targetURL))) + rs.doVisit(t, page, fmt.Sprintf("%s%s%s", GetLoginBaseURL(BaseDomain), "/logout?rd=", url.QueryEscape(targetURL))) if firstFactor { rs.verifyIsFirstFactorPage(t, page) diff --git a/internal/suites/action_visit.go b/internal/suites/action_visit.go index 5fb36b70b..b282ba2a7 100644 --- a/internal/suites/action_visit.go +++ b/internal/suites/action_visit.go @@ -26,11 +26,11 @@ func (rs *RodSession) doVisitAndVerifyOneFactorStep(t *testing.T, page *rod.Page rs.verifyIsFirstFactorPage(t, page) } -func (rs *RodSession) doVisitLoginPage(t *testing.T, page *rod.Page, targetURL string) { +func (rs *RodSession) doVisitLoginPage(t *testing.T, page *rod.Page, baseDomain string, targetURL string) { suffix := "" if targetURL != "" { suffix = fmt.Sprintf("?rd=%s", targetURL) } - rs.doVisitAndVerifyOneFactorStep(t, page, fmt.Sprintf("%s/%s", GetLoginBaseURL(), suffix)) + rs.doVisitAndVerifyOneFactorStep(t, page, fmt.Sprintf("%s/%s", GetLoginBaseURL(baseDomain), suffix)) } diff --git a/internal/suites/const.go b/internal/suites/const.go index 8a8f11cda..45cd89a00 100644 --- a/internal/suites/const.go +++ b/internal/suites/const.go @@ -8,16 +8,38 @@ import ( ) // BaseDomain the base domain. -var BaseDomain = "example.com:8080" +var ( + BaseDomain = "example.com:8080" + Example2DotCom = "example2.com:8080" + Example3DotCom = "example3.com:8080" +) // PathPrefix the prefix/url_base of the login portal. var PathPrefix = os.Getenv("PathPrefix") +// LoginBaseURLFmt the base URL of the login portal for specified baseDomain. +func LoginBaseURLFmt(baseDomain string) string { + if baseDomain == "" { + baseDomain = BaseDomain + } + + return fmt.Sprintf("https://login.%s", baseDomain) +} + // LoginBaseURL the base URL of the login portal. -var LoginBaseURL = fmt.Sprintf("https://login.%s", BaseDomain) +var LoginBaseURL = LoginBaseURLFmt(BaseDomain) + +// SingleFactorBaseURLFmt the base URL of the singlefactor with custom domain. +func SingleFactorBaseURLFmt(baseDomain string) string { + if baseDomain == "" { + baseDomain = BaseDomain + } + + return fmt.Sprintf("https://singlefactor.%s", baseDomain) +} // SingleFactorBaseURL the base URL of the singlefactor domain. -var SingleFactorBaseURL = fmt.Sprintf("https://singlefactor.%s", BaseDomain) +var SingleFactorBaseURL = SingleFactorBaseURLFmt(BaseDomain) // AdminBaseURL the base URL of the admin domain. var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain) diff --git a/internal/suites/environment.go b/internal/suites/environment.go index 9cf51979b..714343779 100644 --- a/internal/suites/environment.go +++ b/internal/suites/environment.go @@ -1,7 +1,6 @@ package suites import ( - "fmt" "os" "strings" "time" @@ -22,7 +21,6 @@ func waitUntilServiceLogDetected( err := utils.CheckUntil(5*time.Second, 1*time.Minute, func() (bool, error) { logs, err := dockerEnvironment.Logs(service, []string{"--tail", "20"}) - fmt.Printf(".") if err != nil { return false, err @@ -35,8 +33,6 @@ func waitUntilServiceLogDetected( return false, nil }) - fmt.Print("\n") - return err } @@ -55,7 +51,7 @@ func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) erro 90*time.Second, dockerEnvironment, "authelia-frontend", - []string{"dev server running at", "ready in"}) + []string{"dev server running at", "ready in", "server restarted"}) } func waitUntilK3DIsReady(dockerEnvironment *DockerEnvironment) error { diff --git a/internal/suites/example/compose/nginx/backend/html/admin/secret.html b/internal/suites/example/compose/nginx/backend/html/admin/secret.html index 3cb5dce0a..9f1e17f7d 100644 --- a/internal/suites/example/compose/nginx/backend/html/admin/secret.html +++ b/internal/suites/example/compose/nginx/backend/html/admin/secret.html @@ -3,6 +3,21 @@