feat(session): multiple session cookie domains (#3754)

This adds support to configure multiple session cookie domains.

Closes #1198

Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
pull/4755/head^2
Manuel Nuñez 2023-01-12 07:57:44 -03:00 committed by GitHub
parent ad1a8042fd
commit 8b29cf7ee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 2715 additions and 1346 deletions

View File

@ -7,7 +7,7 @@ trim_trailing_whitespace = true
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
[*.{sh,yml,yaml}] [*.{html,sh,yml,yaml}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

View File

@ -79,7 +79,7 @@ const (
type labelPriority int type labelPriority int
//nolint:deadcode // Kept for future use. //nolint:deadcode,varcheck // Kept for future use.
const ( const (
labelPriorityCritical labelPriority = iota labelPriorityCritical labelPriority = iota
labelPriorityHigh labelPriorityHigh
@ -122,7 +122,7 @@ func (s labelStatus) String() string {
type labelType int type labelType int
//nolint:deadcode // Kept for future use. //nolint:deadcode,varcheck // Kept for future use.
const ( const (
labelTypeFeature labelType = iota labelTypeFeature labelType = iota
labelTypeBugUnconfirmed labelTypeBugUnconfirmed

View File

@ -114,6 +114,30 @@ var hostEntries = []HostEntry{
{Domain: "redis-sentinel-0.example.com", IP: "192.168.240.120"}, {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-1.example.com", IP: "192.168.240.121"},
{Domain: "redis-sentinel-2.example.com", IP: "192.168.240.122"}, {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) { func runCommand(cmd string, args ...string) {

View File

@ -662,38 +662,76 @@ access_control:
## The session cookies identify the user once logged in. ## The session cookies identify the user once logged in.
## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined. ## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined.
session: 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. ## 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 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 ## 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 ## 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. ## 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. ## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is
expiration: 1h ## 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 ## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is
## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last time ## destroyed if remember me IS NOT selected by the user.
## Authelia detected user activity. expiration: '1h'
inactivity: 5m
## The time before the cookie expires and the session is destroyed if remember me IS selected. ## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed
## Value of -1 disables remember me. ## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie
remember_me_duration: 1M ## domains which do not have a specific 'remember_me' value.
remember_me: '1M'
## ##
## Redis Provider ## Redis Provider

View File

@ -22,7 +22,7 @@ describes the implementation of this. You can use this implementation in various
* session: * session:
* expiration * expiration
* inactivity * inactivity
* remember_me_duration * remember_me
* regulation: * regulation:
* ban_time * ban_time
* find_time * find_time

View File

@ -25,13 +25,21 @@ authenticated user and can then order the reverse proxy to let the request pass
```yaml ```yaml
session: session:
secret: insecure_session_secret
name: authelia_session name: authelia_session
domain: example.com
same_site: lax same_site: lax
secret: unsecure_session_secret
expiration: 1h
inactivity: 5m 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 ## Providers
@ -50,34 +58,6 @@ providers are recommended.
## Options ## 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 ### secret
{{< confkey type="string" required="yes" >}} {{< 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 [Random Alphanumeric String](../../reference/guides/generating-secure-values.md#generating-a-random-alphanumeric-string) with 64 or more
characters. 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 _**Deprecation Notice:** This option is deprecated. See the [cookies](#cookies) section instead._
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 The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root
[remember_me_duration](#remembermeduration) when the remember me box is checked. 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 ### 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 *__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 [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 The default `inactivity` value for all [cookies](#cookies) configurations.
but don't want unused devices to be vulnerable.
### 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" >}} {{< confkey type="duration" default="1M" required="no" >}}
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see *__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 [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 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 ## Security

View File

@ -51,8 +51,8 @@ for, and the structure it must have.
│ └─⫸ Commit Scope: api|autheliabot|authentication|authorization|buildkite|bundler|cmd| │ └─⫸ Commit Scope: api|autheliabot|authentication|authorization|buildkite|bundler|cmd|
│ codecov|commands|configuration|deps|docker|duo|go|golangci-lint| │ codecov|commands|configuration|deps|docker|duo|go|golangci-lint|
│ handlers|logging|metrics|middlewares|mocks|model|notification|npm|ntp| │ handlers|logging|metrics|middlewares|mocks|model|notification|npm|ntp|
│ oidc|regulation|renovate|reviewdog|server|session|storage|suites| │ oidc|random|regulation|renovate|reviewdog|server|session|storage|
│ templates|totp|utils|web suites|templates|totp|utils|web
└─⫸ Commit Type: build|ci|docs|feat|fix|i18n|perf|refactor|release|revert|test └─⫸ Commit Type: build|ci|docs|feat|fix|i18n|perf|refactor|release|revert|test
``` ```
@ -93,6 +93,7 @@ commit messages).
* notification * notification
* ntp * ntp
* oidc * oidc
* random
* regulation * regulation
* server * server
* session * session

View File

@ -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. [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 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 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 [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 manner would mean if the cookie age was more than 2 hours or if the user was inactive for more than 10 minutes the

File diff suppressed because one or more lines are too long

View File

@ -39,12 +39,14 @@ access_control:
policy: two_factor policy: two_factor
session: session:
name: authelia_session
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE # This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
secret: unsecure_session_secret secret: unsecure_session_secret
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes cookies:
domain: example.com # Should match whatever your root protected domain is - name: authelia_session
domain: example.com # Should match whatever your root protected domain is
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
redis: redis:
host: redis host: redis

View File

@ -31,11 +31,13 @@ access_control:
policy: two_factor policy: two_factor
session: session:
name: authelia_session
secret: unsecure_session_secret secret: unsecure_session_secret
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes cookies:
domain: example.com # Should match whatever your root protected domain is - name: authelia_session
domain: example.com # Should match whatever your root protected domain is
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
regulation: regulation:
max_retries: 3 max_retries: 3

View File

@ -662,38 +662,76 @@ access_control:
## The session cookies identify the user once logged in. ## The session cookies identify the user once logged in.
## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined. ## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined.
session: 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. ## 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 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 ## 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 ## 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. ## 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. ## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is
expiration: 1h ## 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 ## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is
## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last time ## destroyed if remember me IS NOT selected by the user.
## Authelia detected user activity. expiration: '1h'
inactivity: 5m
## The time before the cookie expires and the session is destroyed if remember me IS selected. ## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed
## Value of -1 disables remember me. ## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie
remember_me_duration: 1M ## domains which do not have a specific 'remember_me' value.
remember_me: '1M'
## ##
## Redis Provider ## Redis Provider

View File

@ -134,4 +134,11 @@ var deprecations = map[string]Deprecation{
AutoMap: true, AutoMap: true,
MapFunc: nil, 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,
},
} }

View File

@ -323,7 +323,7 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) {
assert.Equal(t, "Admin", config.Notifier.SMTP.Sender.Name) assert.Equal(t, "Admin", config.Notifier.SMTP.Sender.Name)
assert.Equal(t, "admin@example.com", config.Notifier.SMTP.Sender.Address) 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) { func TestShouldParseRegex(t *testing.T) {

View File

@ -105,13 +105,23 @@ var Keys = []string{
"authentication_backend.ldap.permit_feature_detection_failure", "authentication_backend.ldap.permit_feature_detection_failure",
"authentication_backend.ldap.user", "authentication_backend.ldap.user",
"authentication_backend.ldap.password", "authentication_backend.ldap.password",
"session.secret",
"session.name", "session.name",
"session.domain", "session.domain",
"session.same_site", "session.same_site",
"session.secret",
"session.expiration", "session.expiration",
"session.inactivity", "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.host",
"session.redis.port", "session.redis.port",
"session.redis.username", "session.redis.username",

View File

@ -2,6 +2,7 @@ package schema
import ( import (
"crypto/tls" "crypto/tls"
"net/url"
"time" "time"
) )
@ -36,24 +37,42 @@ type RedisSessionConfiguration struct {
// SessionConfiguration represents the configuration related to user sessions. // SessionConfiguration represents the configuration related to user sessions.
type SessionConfiguration struct { type SessionConfiguration struct {
Name string `koanf:"name"` Secret string `koanf:"secret"`
Domain string `koanf:"domain"`
SameSite string `koanf:"same_site"` SessionCookieCommonConfiguration `koanf:",squash"`
Secret string `koanf:"secret"`
Expiration time.Duration `koanf:"expiration"` Cookies []SessionCookieConfiguration `koanf:"cookies"`
Inactivity time.Duration `koanf:"inactivity"`
RememberMeDuration time.Duration `koanf:"remember_me_duration"`
Redis *RedisSessionConfiguration `koanf:"redis"` 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. // DefaultSessionConfiguration is the default session configuration.
var DefaultSessionConfiguration = SessionConfiguration{ var DefaultSessionConfiguration = SessionConfiguration{
Name: "authelia_session", SessionCookieCommonConfiguration: SessionCookieCommonConfiguration{
Expiration: time.Hour, Name: "authelia_session",
Inactivity: time.Minute * 5, Expiration: time.Hour,
RememberMeDuration: time.Hour * 24 * 30, Inactivity: time.Minute * 5,
SameSite: "lax", RememberMe: time.Hour * 24 * 30,
SameSite: "lax",
},
} }
// DefaultRedisConfiguration is the default redis configuration. // DefaultRedisConfiguration is the default redis configuration.

View File

@ -97,7 +97,7 @@ session:
name: authelia_session name: authelia_session
expiration: 3600000 # 1 hour expiration: 3600000 # 1 hour
inactivity: 300000 # 5 minutes inactivity: 300000 # 5 minutes
remember_me_duration: -1 remember_me: -1
domain: example.com domain: example.com
redis: redis:
host: 127.0.0.1 host: 127.0.0.1

View File

@ -25,9 +25,15 @@ func newDefaultConfig() schema.Configuration {
DefaultPolicy: "two_factor", DefaultPolicy: "two_factor",
} }
config.Session = schema.SessionConfiguration{ config.Session = schema.SessionConfiguration{
Domain: examplecom,
Name: "authelia_session",
Secret: "secret", Secret: "secret",
Cookies: []schema.SessionCookieConfiguration{
{
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
Name: "authelia_session",
Domain: exampleDotCom,
},
},
},
} }
config.Storage.EncryptionKey = testEncryptionKey config.Storage.EncryptionKey = testEncryptionKey
config.Storage.Local = &schema.LocalStorageConfiguration{ config.Storage.Local = &schema.LocalStorageConfiguration{
@ -49,6 +55,8 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
ValidateConfiguration(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
config = newDefaultConfig()
config.Notifier.SMTP = nil config.Notifier.SMTP = nil
config.Notifier.FileSystem = 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") 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() validator = schema.NewStructValidator()
config.CertificatesDirectory = "const.go" config.CertificatesDirectory = "const.go"

View File

@ -248,7 +248,7 @@ const (
// Session error constants. // Session error constants.
const ( const (
errFmtSessionOptionRequired = "session: option '%s' is required" 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'" 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" 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'" 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" 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" 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. // Regulation Error Consts.
@ -363,7 +373,10 @@ var (
validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} 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{ var replacedKeys = map[string]string{
"authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify", "authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify",

View File

@ -12,5 +12,5 @@ const (
) )
const ( const (
examplecom = "example.com" exampleDotCom = "example.com"
) )

View File

@ -257,7 +257,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
}, },
SectorIdentifier: mustParseURL(examplecom), SectorIdentifier: mustParseURL(exampleDotCom),
}, },
}, },
}, },
@ -289,12 +289,12 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
}, },
}, },
Errors: []string{ 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", exampleDotCom, "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", exampleDotCom, "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", exampleDotCom, "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", exampleDotCom, "fragment", "fragment"),
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "username", "user"), 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", examplecom, "password"), fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "password"),
}, },
}, },
{ {

View File

@ -23,7 +23,7 @@ func (suite *NotifierSuite) SetupTest() {
Username: "john", Username: "john",
Password: "password", Password: "password",
Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"}, Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"},
Host: examplecom, Host: exampleDotCom,
Port: 25, Port: 25,
} }
suite.config.FileSystem = nil suite.config.FileSystem = nil
@ -78,7 +78,7 @@ func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() {
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 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().Equal(uint16(tls.VersionTLS12), suite.config.SMTP.TLS.MinimumVersion.Value)
suite.Assert().False(suite.config.SMTP.TLS.SkipVerify) suite.Assert().False(suite.config.SMTP.TLS.SkipVerify)
} }
@ -111,7 +111,7 @@ func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() {
} }
func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() { func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() {
suite.config.SMTP.Host = examplecom suite.config.SMTP.Host = exampleDotCom
suite.config.SMTP.TLS = &schema.TLSConfig{ suite.config.SMTP.TLS = &schema.TLSConfig{
MinimumVersion: schema.TLSVersion{Value: tls.VersionSSL30}, //nolint:staticcheck MinimumVersion: schema.TLSVersion{Value: tls.VersionSSL30}, //nolint:staticcheck
} }
@ -125,7 +125,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() {
} }
func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() { func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() {
suite.config.SMTP.Host = examplecom suite.config.SMTP.Host = exampleDotCom
suite.config.SMTP.TLS = &schema.TLSConfig{ suite.config.SMTP.TLS = &schema.TLSConfig{
MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13}, MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13},
MaximumVersion: schema.TLSVersion{Value: tls.VersionTLS10}, MaximumVersion: schema.TLSVersion{Value: tls.VersionTLS10},
@ -140,7 +140,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() {
} }
func (suite *NotifierSuite) TestSMTPShouldWarnOnDisabledSTARTTLS() { func (suite *NotifierSuite) TestSMTPShouldWarnOnDisabledSTARTTLS() {
suite.config.SMTP.Host = examplecom suite.config.SMTP.Host = exampleDotCom
suite.config.SMTP.DisableStartTLS = true suite.config.SMTP.DisableStartTLS = true
ValidateNotifier(&suite.config, suite.validator) ValidateNotifier(&suite.config, suite.validator)

View File

@ -35,18 +35,11 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru
config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min.
} }
if config.RememberMeDuration <= 0 && config.RememberMeDuration != schema.RememberMeDisabled { switch {
config.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month. case config.RememberMe == schema.RememberMeDisabled:
} config.DisableRememberMe = true
case config.RememberMe <= 0:
if config.Domain == "" { config.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month.
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))
} }
if config.SameSite == "" { if config.SameSite == "" {
@ -54,6 +47,137 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru
} else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) { } else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
validator.Push(fmt.Errorf(errFmtSessionSameSite, strings.Join(validSessionSameSiteValues, "', '"), config.SameSite)) 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) { func validateRedisCommon(config *schema.SessionConfiguration, validator *schema.StructValidator) {

View File

@ -3,7 +3,9 @@ package validator
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/url"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -14,7 +16,8 @@ import (
func newDefaultSessionConfig() schema.SessionConfiguration { func newDefaultSessionConfig() schema.SessionConfiguration {
config := schema.SessionConfiguration{} config := schema.SessionConfiguration{}
config.Secret = testJWTSecret config.Secret = testJWTSecret
config.Domain = examplecom config.Domain = exampleDotCom
config.Cookies = []schema.SessionCookieConfiguration{}
return config return config
} }
@ -30,15 +33,144 @@ func TestShouldSetDefaultSessionValues(t *testing.T) {
assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name) assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name)
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) 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) 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) { func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
config.Expiration, config.Inactivity, config.RememberMeDuration = -1, -1, -2 config.Expiration, config.Inactivity, config.RememberMe = -1, -1, -2
ValidateSession(&config, validator) ValidateSession(&config, validator)
@ -46,7 +178,7 @@ func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) 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) { func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) {
@ -60,7 +192,7 @@ func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) {
require.Len(t, validator.Warnings(), 1) require.Len(t, validator.Warnings(), 1)
assert.Len(t, validator.Errors(), 0) 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) { func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
@ -72,6 +204,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
validator.Clear() validator.Clear()
config = newDefaultSessionConfig()
// Set redis config because password must be set only when redis is used. // Set redis config because password must be set only when redis is used.
config.Redis = &schema.RedisSessionConfiguration{ config.Redis = &schema.RedisSessionConfiguration{
Host: "redis.localhost", Host: "redis.localhost",
@ -81,8 +215,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) assert.Len(t, validator.Warnings(), 0)
assert.False(t, validator.HasErrors()) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, 8, config.Redis.MaximumActiveConnections) assert.Equal(t, 8, config.Redis.MaximumActiveConnections)
} }
@ -98,7 +232,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) require.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, -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) assert.Len(t, validator.Errors(), 0)
validator.Clear() validator.Clear()
config = newDefaultSessionConfig()
config.Secret = ""
// Set redis config because password must be set only when redis is used. // Set redis config because password must be set only when redis is used.
config.Redis = &schema.RedisSessionConfiguration{ config.Redis = &schema.RedisSessionConfiguration{
Host: "redis.localhost", Host: "redis.localhost",
@ -153,6 +290,8 @@ func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
validator.Clear() validator.Clear()
config = newDefaultSessionConfig()
// Set redis config because password must be set only when redis is used. // Set redis config because password must be set only when redis is used.
config.Redis = &schema.RedisSessionConfiguration{ config.Redis = &schema.RedisSessionConfiguration{
Host: "redis.localhost", Host: "redis.localhost",
@ -287,7 +426,24 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi
validator.Clear() 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) ValidateSession(&config, validator)
@ -434,6 +590,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
config.Domain = "" config.Domain = ""
config.Cookies = []schema.SessionCookieConfiguration{}
ValidateSession(&config, validator) ValidateSession(&config, validator)
@ -449,9 +606,141 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
ValidateSession(&config, validator) 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.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) 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) { func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
@ -462,22 +751,28 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) 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()[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) { func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
var config schema.SessionConfiguration
validOptions := []string{"none", "lax", "strict"} validOptions := []string{"none", "lax", "strict"}
for _, opt := range validOptions { for _, opt := range validOptions {
validator.Clear()
config = newDefaultSessionConfig()
config.SameSite = opt config.SameSite = opt
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
} }
} }
@ -487,7 +782,7 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
config.Inactivity = -1 config.Inactivity = -1
config.Expiration = -1 config.Expiration = -1
config.RememberMeDuration = schema.RememberMeDisabled config.RememberMe = schema.RememberMeDisabled
ValidateSession(&config, validator) ValidateSession(&config, validator)
@ -496,7 +791,8 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) 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) { func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
@ -505,7 +801,41 @@ func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) assert.Len(t, validator.Warnings(), 0)
assert.False(t, validator.HasErrors()) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration)
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
} }

View File

@ -112,6 +112,7 @@ const (
testInactivity = time.Second * 10 testInactivity = time.Second * 10
testRedirectionURL = "http://redirection.local" testRedirectionURL = "http://redirection.local"
testUsername = "john" testUsername = "john"
exampleDotCom = "example.com"
) )
// Duo constants. // Duo constants.

View File

@ -2,9 +2,9 @@ package handlers
import ( import (
"fmt" "fmt"
"net/url"
"github.com/authelia/authelia/v4/internal/middlewares" "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. // 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 return
} }
var reqBody checkURIWithinDomainRequestBody var (
bodyJSON checkURIWithinDomainRequestBody
targetURI *url.URL
err error
)
err := ctx.ParseBody(&reqBody) if err = ctx.ParseBody(&bodyJSON); err != nil {
if err != nil {
ctx.Error(fmt.Errorf("unable to parse request body: %w", err), messageOperationFailed) ctx.Error(fmt.Errorf("unable to parse request body: %w", err), messageOperationFailed)
return return
} }
safe, err := utils.IsURIStringSafeRedirection(reqBody.URI, ctx.Configuration.Session.Domain) if targetURI, err = url.ParseRequestURI(bodyJSON.URI); err != nil {
if 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)
ctx.Error(fmt.Errorf("unable to determine if uri %s is safe to redirect to: %w", reqBody.URI, err), messageOperationFailed)
return return
} }
err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{ if err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{OK: ctx.IsSafeRedirectionTargetURI(targetURI)}); err != nil {
OK: safe,
})
if err != nil {
ctx.Error(fmt.Errorf("unable to create response body: %w", err), messageOperationFailed) ctx.Error(fmt.Errorf("unable to create response body: %w", err), messageOperationFailed)
return return
} }

View File

@ -4,64 +4,78 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session" "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) { for _, tc := range testCases {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{ t.Run(tc.name, func(t *testing.T) {
Username: "john", mock := mocks.NewMockAutheliaCtxWithUserSession(t, tc.userSession)
AuthenticationLevel: authentication.NotAuthenticated, defer mock.Close()
})
defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
URI: "http://myapp.example.com", URI: tc.have,
}) })
CheckSafeRedirectionPOST(mock.Ctx) CheckSafeRedirectionPOST(mock.Ctx)
assert.Equal(t, 401, mock.Ctx.Response.StatusCode())
}
func TestCheckSafeRedirection_UnsafeRedirection(t *testing.T) { assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})
defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ if tc.expected == fasthttp.StatusOK {
URI: "http://myapp.com", mock.Assert200OK(t, checkURIWithinDomainResponseBody{
}) OK: tc.ok,
})
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,
})
} }
func TestShouldFailOnInvalidBody(t *testing.T) { func TestShouldFailOnInvalidBody(t *testing.T) {
@ -70,7 +84,7 @@ func TestShouldFailOnInvalidBody(t *testing.T) {
AuthenticationLevel: authentication.OneFactor, AuthenticationLevel: authentication.OneFactor,
}) })
defer mock.Close() defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain mock.Ctx.Configuration.Session.Domain = exampleDotCom
mock.SetRequestBody(t, "not a valid json") mock.SetRequestBody(t, "not a valid json")
@ -84,7 +98,7 @@ func TestShouldFailOnInvalidURL(t *testing.T) {
AuthenticationLevel: authentication.OneFactor, AuthenticationLevel: authentication.OneFactor,
}) })
defer mock.Close() defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain mock.Ctx.Configuration.Session.Domain = exampleDotCom
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{ mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
URI: "https//invalid-url", URI: "https//invalid-url",

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"time" "time"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
@ -72,7 +71,25 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
return 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() newSession := session.NewDefaultUserSession()
// Reset all values from previous session except OIDC workflow before regenerating the cookie. // 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 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) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthType1FA, bodyJSON.Username, err)
respondUnauthorized(ctx, messageAuthenticationFailed) 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. // 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. // Set the cookie to expire if remember me is enabled and the user has asked us to.
if keepMeLoggedIn { if keepMeLoggedIn {
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe) err = provider.UpdateExpiration(ctx.RequestCtx, provider.Config.RememberMe)
if err != nil { if err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "updated expiration", regulation.AuthType1FA, bodyJSON.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "updated expiration", regulation.AuthType1FA, bodyJSON.Username, err)

View File

@ -5,7 +5,6 @@ import (
"net/url" "net/url"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils"
) )
type logoutBody struct { 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) 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 { if err != nil {
ctx.Error(fmt.Errorf("unable to destroy session during logout: %s", err), messageOperationFailed) ctx.Error(fmt.Errorf("unable to destroy session during logout: %s", err), messageOperationFailed)
} }
redirectionURL, err := url.ParseRequestURI(body.TargetURL) redirectionURL, err := url.ParseRequestURI(body.TargetURL)
if err == nil { if err == nil {
responseBody.SafeTargetURL = utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) responseBody.SafeTargetURL = ctx.IsSafeRedirectionTargetURI(redirectionURL)
} }
if body.TargetURL != "" { if body.TargetURL != "" {

View File

@ -246,7 +246,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) { func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) {
userSession := ctx.GetSession() userSession := ctx.GetSession()
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err := ctx.RegenerateSession()
if err != nil { if err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username, err)

View File

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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/duo"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
@ -579,6 +580,18 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() { func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl) 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(). s.mock.StorageMock.EXPECT().
LoadPreferredDuoDevice(s.mock.Ctx, "john"). LoadPreferredDuoDevice(s.mock.Ctx, "john").
Return(&model.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil) Return(&model.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)

View File

@ -50,7 +50,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
return 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) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeTOTP, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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/mocks"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
@ -143,6 +144,18 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() { func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
config := model.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"} 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(). s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).

View File

@ -171,7 +171,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return 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) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -259,9 +259,11 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
} }
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
sessionConfig := mock.Ctx.Configuration.Session
if resp.config != nil { if resp.config != nil {
mock.Ctx.Configuration = *resp.config mock.Ctx.Configuration = *resp.config
mock.Ctx.Configuration.Session = sessionConfig
} }
// Set the initial user session. // Set the initial user session.

View File

@ -19,14 +19,6 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "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 { func isSchemeWSS(url *url.URL) bool {
return url.Scheme == "wss" 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. // 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 { username string, userGroups []string, clientIP net.IP, method []byte, authLevel authentication.Level) authorizationMatching {
hasSubject, level := authorizer.GetRequiredLevel( hasSubject, level := authorizer.GetRequiredLevel(
authorization.Subject{ authorization.Subject{
@ -62,7 +54,7 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U
Groups: userGroups, Groups: userGroups,
IP: clientIP, IP: clientIP,
}, },
authorization.NewObjectRaw(&targetURL, method)) authorization.NewObjectRaw(targetURL, method))
switch { switch {
case level == authorization.Bypass: 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) { 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 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 return isInactiveTooLong
} }
@ -151,7 +148,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) { if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) {
// Destroy the session a new one will be regenerated on next request. // 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) 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 = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil {
if err == authentication.ErrUserNotFound { 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) 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 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 ( var (
statusCode int statusCode int
friendlyUsername string friendlyUsername string
@ -211,8 +208,8 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
redirectionURL := ctxGetPortalURL(ctx) redirectionURL := ctxGetPortalURL(ctx)
if redirectionURL != nil { if redirectionURL != nil {
if !utils.IsURISafeRedirection(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, 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, cookieDomain)
ctx.ReplyUnauthorized() ctx.ReplyUnauthorized()
@ -414,6 +411,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
} }
userSession := ctx.GetSession() userSession := ctx.GetSession()
if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil { if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil {
return isBasicAuth, username, name, groups, emails, authLevel, err 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) { 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") 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) 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 return
} }
if !isSchemeHTTPS(targetURL) && !isSchemeWSS(targetURL) { if !utils.IsURISecure(targetURL) {
ctx.Logger.Errorf("Scheme of target URL %s must be secure since cookies are "+ 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()) "only transported over a secure connection for security reasons", targetURL.String())
ctx.ReplyUnauthorized() ctx.ReplyUnauthorized()
@ -455,14 +453,32 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
return return
} }
if !isURLUnderProtectedDomain(targetURL, ctx.Configuration.Session.Domain) { cookieDomain := ctx.GetTargetURICookieDomain(targetURL)
ctx.Logger.Errorf("Target URL %s is not under the protected domain %s",
targetURL.String(), ctx.Configuration.Session.Domain) 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() ctx.ReplyUnauthorized()
return return
} }
ctx.Logger.Debugf("Target URL '%s' was detected as a match to the '%s' session cookie domain", targetURL.String(), cookieDomain)
method := ctx.XForwardedMethod() method := ctx.XForwardedMethod()
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval) isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
@ -474,12 +490,12 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
return return
} }
handleUnauthorized(ctx, targetURL, isBasicAuth, username, method) handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
return return
} }
authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, *targetURL, username, authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, targetURL, username,
groups, ctx.RemoteIP(), method, authLevel) groups, ctx.RemoteIP(), method, authLevel)
switch authorized { 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.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
ctx.ReplyForbidden() ctx.ReplyForbidden()
case NotAuthorized: case NotAuthorized:
handleUnauthorized(ctx, targetURL, isBasicAuth, username, method) handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
case Authorized: case Authorized:
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails) setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
} }

View File

@ -43,6 +43,8 @@ func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) { func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
defer mock.Close() defer mock.Close()
_, err := mock.Ctx.GetOriginalURL() _, err := mock.Ctx.GetOriginalURL()
assert.Error(t, err) assert.Error(t, err)
@ -53,6 +55,7 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
_, err := mock.Ctx.GetOriginalURL() _, err := mock.Ctx.GetOriginalURL()
assert.Error(t, err) assert.Error(t, err)
@ -162,7 +165,7 @@ func TestShouldCheckAuthorizationMatching(t *testing.T) {
username = testUsername 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", assert.Equal(t, rule.ExpectedMatching, matching, "policy=%s, authLevel=%v, expected=%v, actual=%v",
rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching) rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching)
} }
@ -510,7 +513,6 @@ func TestShouldNotCrashOnEmptyEmail(t *testing.T) {
userSession.AuthenticationLevel = authentication.OneFactor userSession.AuthenticationLevel = authentication.OneFactor
userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
fmt.Printf("Time is %v\n", userSession.RefreshTTL)
err := mock.Ctx.SaveSession(userSession) err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err) 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}, {"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403},
} }
for _, testCase := range testCases { for i, tc := range testCases {
testCase := testCase t.Run(tc.String(), func(t *testing.T) {
t.Run(testCase.String(), func(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
mock.Clock.Set(time.Now()) mock.Clock.Set(time.Now())
mock.Ctx.Request.Header.Set("X-Original-URL", tc.URL)
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
userSession.Username = testCase.Username userSession.Username = tc.Username
userSession.Emails = testCase.Emails userSession.Emails = tc.Emails
userSession.AuthenticationLevel = testCase.AuthenticationLevel userSession.AuthenticationLevel = tc.AuthenticationLevel
userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute) userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
err := mock.Ctx.SaveSession(userSession) err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err) require.NoError(t, err)
mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL)
VerifyGET(verifyGetCfg)(mock.Ctx) 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", 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 != "" { fmt.Println(i)
assert.Equal(t, []byte(testCase.Username), mock.Ctx.Response.Header.Peek("Remote-User")) 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")) assert.Equal(t, []byte("john.doe@example.com"), mock.Ctx.Response.Header.Peek("Remote-Email"))
} else { } else {
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User")) 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()) clock.Set(time.Now())
past := clock.Now().Add(-1 * time.Hour) 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. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) 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 := mock.Ctx.GetSession()
userSession.Username = testUsername userSession.Username = testUsername
@ -719,8 +724,6 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
err := mock.Ctx.SaveSession(userSession) err := mock.Ctx.SaveSession(userSession)
require.NoError(t, err) require.NoError(t, err)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
VerifyGET(verifyGetCfg)(mock.Ctx) VerifyGET(verifyGetCfg)(mock.Ctx)
// The session has been destroyed. // The session has been destroyed.
@ -739,10 +742,10 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
clock := utils.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) 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. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) 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 := mock.Ctx.GetSession()
userSession.Username = testUsername userSession.Username = testUsername
@ -768,7 +771,7 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te
mock.Clock.Set(time.Now()) mock.Clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = testInactivity mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
userSession.Username = testUsername userSession.Username = testUsername
@ -800,7 +803,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T)
mock.Clock.Set(time.Now()) 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) past := mock.Clock.Now().Add(-1 * time.Hour)
@ -836,10 +839,10 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
clock := utils.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) 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. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) 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) past := clock.Now().Add(-1 * time.Hour)
@ -899,7 +902,7 @@ func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *tes
mock.Clock.Set(time.Now()) 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) past := mock.Clock.Now().Add(-1 * time.Hour)
@ -974,47 +977,6 @@ func TestShouldURLEncodeRedirectionHeader(t *testing.T) {
string(mock.Ctx.Response.Body())) 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) { func TestSchemeIsWSS(t *testing.T) {
GetURL := func(u string) *url.URL { GetURL := func(u string) *url.URL {
x, err := url.ParseRequestURI(u) x, err := url.ParseRequestURI(u)
@ -1435,10 +1397,10 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.
clock.Set(time.Now()) clock.Set(time.Now())
past := clock.Now().Add(-1 * time.Hour) 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. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil) 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 := mock.Ctx.GetSession()
userSession.Username = testUsername userSession.Username = testUsername
@ -1527,7 +1489,7 @@ func TestIsSessionInactiveTooLong(t *testing.T) {
defer ctx.Close() 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.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil)
ctx.Clock.Set(tc.now) ctx.Clock.Set(tc.now)

View File

@ -13,7 +13,6 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils"
) )
// Handle1FAResponse handle the redirection upon 1FA authentication. // Handle1FAResponse handle the redirection upon 1FA authentication.
@ -57,7 +56,7 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
return return
} }
if !utils.IsURISafeRedirection(targetURL, ctx.Configuration.Session.Domain) { if !ctx.IsSafeRedirectionTargetURI(targetURL) {
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI) ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
@ -98,14 +97,18 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
return return
} }
var safe bool var (
parsedURI *url.URL
if safe, err = utils.IsURIStringSafeRedirection(targetURI, ctx.Configuration.Session.Domain); err != nil { safe bool
ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed) )
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 return
} }
safe = ctx.IsSafeRedirectionTargetURI(parsedURI)
if safe { if safe {
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI) ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)

View File

@ -146,6 +146,7 @@ func TestWebauthnGetUserWithErr(t *testing.T) {
func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) { func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
ctx.Ctx.Request.Header.Del("X-Forwarded-Host")
w, err := newWebauthn(ctx.Ctx) w, err := newWebauthn(ctx.Ctx)

View File

@ -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. // GetSession return the user session. Any update will be saved in cache.
func (ctx *AutheliaCtx) GetSession() session.UserSession { 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 { if err != nil {
ctx.Logger.Error("Unable to retrieve user session") ctx.Logger.Error("Unable to retrieve user session")
return session.NewDefaultUserSession() return session.NewDefaultUserSession()
@ -240,7 +294,32 @@ func (ctx *AutheliaCtx) GetSession() session.UserSession {
// SaveSession save the content of the session. // SaveSession save the content of the session.
func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error { 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. // ReplyOK is a helper method to reply ok.

View File

@ -182,6 +182,8 @@ func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) { func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
defer mock.Close() defer mock.Close()
mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password") mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password")
@ -196,6 +198,8 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password") mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password")
mock.Ctx.RequestCtx.Request.SetHost("localhost") mock.Ctx.RequestCtx.Request.SetHost("localhost")
mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234") mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234")

View File

@ -51,9 +51,25 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia.Clock.Set(datetime) mockAuthelia.Clock.Set(datetime)
config := schema.Configuration{} config := schema.Configuration{}
config.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration config.Session.Cookies = []schema.SessionCookieConfiguration{
config.Session.Name = "authelia_session" {
config.Session.Domain = "example.com" 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.DefaultPolicy = "deny"
config.AccessControl.Rules = []schema.ACLRule{{ config.AccessControl.Rules = []schema.ACLRule{{
Domains: []string{"bypass.example.com"}, 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. // Set a cookie to identify this client throughout the test.
// request.Request.Header.SetCookie("authelia_session", "client_cookie"). // 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) ctx := middlewares.NewAutheliaCtx(request, config, providers)
mockAuthelia.Ctx = ctx mockAuthelia.Ctx = ctx

View File

@ -139,8 +139,6 @@ func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject
var client *gomail.Client 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 { if client, err = gomail.NewClient(n.config.Host, n.opts...); err != nil {
return fmt.Errorf("notifier: smtp: failed to establish client: %w", err) return fmt.Errorf("notifier: smtp: failed to establish client: %w", err)
} }

View File

@ -0,0 +1,9 @@
{
"Base":"{{ .Base }}",
"DuoSelfEnrollment":"{{ .DuoSelfEnrollment }}",
"LogoOverride":"{{ .LogoOverride }}",
"RememberMe":"{{ .RememberMe }}",
"ResetPassword":"{{ .ResetPassword }}",
"ResetPasswordCustomURL":"{{ .ResetPasswordCustomURL }}",
"Theme":"{{ .Theme }}"
}

View File

@ -16,6 +16,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/random" "github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/templates" "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)) 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.RequestCtx.Error("an error occurred", 503)
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
@ -190,7 +200,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
opts = &TemplatedFileOptions{ opts = &TemplatedFileOptions{
AssetPath: config.Server.AssetPath, AssetPath: config.Server.AssetPath,
DuoSelfEnrollment: strFalse, DuoSelfEnrollment: strFalse,
RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled), RememberMe: strconv.FormatBool(!config.Session.DisableRememberMe),
ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable), ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable),
ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(), ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(),
Theme: config.Theme, Theme: config.Theme,
@ -227,7 +237,11 @@ type TemplatedFileOptions struct {
} }
// CommonData returns a TemplatedFileCommonData with the dynamic options. // 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{ return TemplatedFileCommonData{
Base: base, Base: base,
BaseURL: baseURL, 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. // OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options.
func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData { func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData {
return TemplatedFileOpenAPIData{ return TemplatedFileOpenAPIData{

View File

@ -9,6 +9,12 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "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. // EncryptingSerializer a serializer encrypting the data with AES-GCM with 256-bit keys.
type EncryptingSerializer struct { type EncryptingSerializer struct {
key [32]byte key [32]byte
@ -21,7 +27,7 @@ func NewEncryptingSerializer(secret string) *EncryptingSerializer {
} }
// Encode encode and encrypt session. // 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 { if len(src.KV) == 0 {
return nil, nil 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) return nil, fmt.Errorf("unable to marshal session: %v", err)
} }
encryptedDst, err := utils.Encrypt(dst, &e.key) if data, err = utils.Encrypt(dst, &e.key); err != nil {
if err != nil {
return nil, fmt.Errorf("unable to encrypt session: %v", err) return nil, fmt.Errorf("unable to encrypt session: %v", err)
} }
return encryptedDst, nil return data, nil
} }
// Decode decrypt and decode session. // 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 { if len(src) == 0 {
return nil return nil
} }
@ -49,12 +54,13 @@ func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error {
delete(dst.KV, k) delete(dst.KV, k)
} }
decryptedSrc, err := utils.Decrypt(src, &e.key) var data []byte
if err != nil {
if data, err = utils.Decrypt(src, &e.key); err != nil {
return fmt.Errorf("unable to decrypt session: %s", err) return fmt.Errorf("unable to decrypt session: %s", err)
} }
_, err = dst.UnmarshalMsg(decryptedSrc) _, err = dst.UnmarshalMsg(data)
return err return err
} }

View File

@ -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)
}

View File

@ -2,158 +2,61 @@ package session
import ( import (
"crypto/x509" "crypto/x509"
"encoding/json" "fmt"
"time"
fasthttpsession "github.com/fasthttp/session/v2" "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/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
) )
// Provider a session provider. // Provider contains a list of domain sessions.
type Provider struct { type Provider struct {
sessionHolder *fasthttpsession.Session sessions map[string]*Session
RememberMe time.Duration
Inactivity time.Duration
} }
// NewProvider instantiate a session provider given a configuration. // NewProvider instantiate a session provider given a configuration.
func NewProvider(config schema.SessionConfiguration, certPool *x509.CertPool) *Provider { func NewProvider(config schema.SessionConfiguration, certPool *x509.CertPool) *Provider {
c := NewProviderConfig(config, certPool) log := logging.Logger()
provider := new(Provider) name, p, s, err := NewSessionProvider(config, certPool)
provider.sessionHolder = fasthttpsession.New(c.config) if err != nil {
log.Fatal(err)
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)
}
} }
err = provider.sessionHolder.SetProvider(providerImpl) provider := &Provider{
if err != nil { sessions: map[string]*Session{},
logger.Fatal(err) }
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 return provider
} }
// GetSession return the user session from a request. // Get returns session information for specified domain.
func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) { func (p *Provider) Get(domain string) (*Session, error) {
store, err := p.sessionHolder.Get(ctx) if domain == "" {
return nil, fmt.Errorf("can not get session from an undefined domain")
if err != nil {
return NewDefaultUserSession(), err
} }
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 if !found {
// and save it in the store. return nil, fmt.Errorf("no session found for domain '%s'", domain)
if !ok {
userSession := NewDefaultUserSession()
store.Set(userSessionStorerKey, userSession)
return userSession, nil
} }
var userSession UserSession return s, nil
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
} }

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/fasthttp/session/v2" "github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/memory"
"github.com/fasthttp/session/v2/providers/redis" "github.com/fasthttp/session/v2/providers/redis"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -18,7 +19,7 @@ import (
) )
// NewProviderConfig creates a configuration for creating the session provider. // 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 := session.NewDefaultConfig()
c.SessionIDGeneratorFunc = func() []byte { c.SessionIDGeneratorFunc = func() []byte {
@ -61,16 +62,42 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
return true 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. // If redis configuration is provided, then use the redis provider.
switch { switch {
case config.Redis != nil: case config.Redis != nil:
serializer := NewEncryptingSerializer(config.Secret) serializer = NewEncryptingSerializer(config.Secret)
var tlsConfig *tls.Config var tlsConfig *tls.Config
@ -92,8 +119,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
} }
} }
providerName = "redis-sentinel" name = "redis-sentinel"
redisSentinelConfig = &redis.FailoverConfig{
provider, err = redis.NewFailoverCluster(redis.FailoverConfig{
Logger: logging.LoggerCtxPrintf(logrus.TraceLevel), Logger: logging.LoggerCtxPrintf(logrus.TraceLevel),
MasterName: config.Redis.HighAvailability.SentinelName, MasterName: config.Redis.HighAvailability.SentinelName,
SentinelAddrs: addrs, SentinelAddrs: addrs,
@ -109,9 +137,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
IdleTimeout: 300, IdleTimeout: 300,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
KeyPrefix: "authelia-session", KeyPrefix: "authelia-session",
} })
} else { } else {
providerName = "redis" name = "redis"
network := "tcp" network := "tcp"
var addr string 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) 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), Logger: logging.LoggerCtxPrintf(logrus.TraceLevel),
Network: network, Network: network,
Addr: addr, Addr: addr,
@ -135,19 +163,12 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
IdleTimeout: 300, IdleTimeout: 300,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
KeyPrefix: "authelia-session", KeyPrefix: "authelia-session",
} })
} }
c.EncodeFunc = serializer.Encode
c.DecodeFunc = serializer.Decode
default: default:
providerName = "memory" name = "memory"
provider, err = memory.New(memory.Config{})
} }
return ProviderConfig{ return name, provider, serializer, err
c,
redisConfig,
redisSentinelConfig,
providerName,
}
} }

View File

@ -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"])
}

View File

@ -5,7 +5,6 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
@ -14,16 +13,31 @@ import (
"github.com/authelia/authelia/v4/internal/oidc" "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) { func TestShouldInitializerSession(t *testing.T) {
ctx := &fasthttp.RequestCtx{} 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) session, err := provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, NewDefaultUserSession(), session) assert.Equal(t, NewDefaultUserSession(), session)
} }
@ -31,22 +45,19 @@ func TestShouldInitializerSession(t *testing.T) {
func TestShouldUpdateSession(t *testing.T) { func TestShouldUpdateSession(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{} provider, err := newTestSession()
configuration.Domain = testDomain assert.NoError(t, err)
configuration.Name = testName
configuration.Expiration = testExpiration
provider := NewProvider(configuration, nil)
session, _ := provider.GetSession(ctx) session, _ := provider.GetSession(ctx)
session.Username = testUsername session.Username = testUsername
session.AuthenticationLevel = authentication.TwoFactor session.AuthenticationLevel = authentication.TwoFactor
err := provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, UserSession{ assert.Equal(t, UserSession{
Username: testUsername, Username: testUsername,
@ -56,26 +67,23 @@ func TestShouldUpdateSession(t *testing.T) {
func TestShouldSetSessionAuthenticationLevels(t *testing.T) { func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{}
timeOneFactor := time.Unix(1625048140, 0) timeOneFactor := time.Unix(1625048140, 0)
timeTwoFactor := time.Unix(1625048150, 0) timeTwoFactor := time.Unix(1625048150, 0)
timeZeroFactor := time.Unix(0, 0) timeZeroFactor := time.Unix(0, 0)
configuration.Domain = testDomain provider, err := newTestSession()
configuration.Name = testName assert.NoError(t, err)
configuration.Expiration = testExpiration
provider := NewProvider(configuration, nil)
session, _ := provider.GetSession(ctx) session, _ := provider.GetSession(ctx)
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false) session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
err := provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
authAt, err := session.AuthenticatedTime(authorization.OneFactor) authAt, err := session.AuthenticatedTime(authorization.OneFactor)
assert.NoError(t, err) assert.NoError(t, err)
@ -100,10 +108,10 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
session.SetTwoFactorDuo(timeTwoFactor) session.SetTwoFactorDuo(timeTwoFactor)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, UserSession{ assert.Equal(t, UserSession{
Username: testUsername, Username: testUsername,
@ -129,26 +137,23 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) { func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{}
timeOneFactor := time.Unix(1625048140, 0) timeOneFactor := time.Unix(1625048140, 0)
timeTwoFactor := time.Unix(1625048150, 0) timeTwoFactor := time.Unix(1625048150, 0)
timeZeroFactor := time.Unix(0, 0) timeZeroFactor := time.Unix(0, 0)
configuration.Domain = testDomain provider, err := newTestSession()
configuration.Name = testName assert.NoError(t, err)
configuration.Expiration = testExpiration
provider := NewProvider(configuration, nil)
session, _ := provider.GetSession(ctx) session, _ := provider.GetSession(ctx)
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false) session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
err := provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
authAt, err := session.AuthenticatedTime(authorization.OneFactor) authAt, err := session.AuthenticatedTime(authorization.OneFactor)
assert.NoError(t, err) assert.NoError(t, err)
@ -173,10 +178,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) 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.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs)
assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication()) assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())
@ -196,10 +201,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
@ -208,10 +213,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
@ -220,10 +225,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
@ -232,10 +237,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
@ -244,10 +249,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
@ -256,10 +261,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
@ -268,10 +273,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
@ -280,10 +285,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
@ -292,31 +297,28 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) { func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{} domainSession, err := newTestSession()
configuration.Domain = testDomain assert.NoError(t, err)
configuration.Name = testName
configuration.Expiration = testExpiration
provider := NewProvider(configuration, nil) session, err := domainSession.GetSession(ctx)
session, err := provider.GetSession(ctx) assert.NoError(t, err)
require.NoError(t, err)
session.Username = testUsername session.Username = testUsername
session.AuthenticationLevel = authentication.TwoFactor session.AuthenticationLevel = authentication.TwoFactor
err = provider.SaveSession(ctx, session) err = domainSession.SaveSession(ctx, session)
require.NoError(t, err) assert.NoError(t, err)
newUserSession, err := provider.GetSession(ctx) newUserSession, err := domainSession.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testUsername, newUserSession.Username) assert.Equal(t, testUsername, newUserSession.Username)
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel) assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
err = provider.DestroySession(ctx) err = domainSession.DestroySession(ctx)
require.NoError(t, err) assert.NoError(t, err)
newUserSession, err = provider.GetSession(ctx) newUserSession, err = domainSession.GetSession(ctx)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "", newUserSession.Username) assert.Equal(t, "", newUserSession.Username)
assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel) assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
} }

View File

@ -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
}

View File

@ -3,8 +3,7 @@ package session
import ( import (
"time" "time"
session "github.com/fasthttp/session/v2" "github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
@ -13,10 +12,8 @@ import (
// ProviderConfig is the configuration used to create the session provider. // ProviderConfig is the configuration used to create the session provider.
type ProviderConfig struct { type ProviderConfig struct {
config session.Config config session.Config
redisConfig *redis.Config providerName string
redisSentinelConfig *redis.FailoverConfig
providerName string
} }
// UserSession is the structure representing the session of a user. // UserSession is the structure representing the session of a user.

View File

@ -34,7 +34,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -23,7 +23,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -20,10 +20,13 @@ authentication_backend:
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com cookies:
expiration: 3600 # 1 hour - name: 'authelia_session'
inactivity: 300 # 5 minutes domain: 'example.com'
remember_me_duration: 1y authelia_url: 'https://login.example.com'
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -21,10 +21,12 @@ authentication_backend:
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
cookies:
- name: 'authelia_session'
domain: 'example.com'
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -23,7 +23,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -102,7 +102,7 @@ session:
- host: redis-sentinel-2 - host: redis-sentinel-2
port: 26379 port: 26379
remember_me_duration: 1y remember_me: 1y
regulation: regulation:
max_retries: 3 max_retries: 3

View File

@ -38,7 +38,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -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
...

View File

@ -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'
...

View File

@ -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
...

View File

@ -25,7 +25,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -23,7 +23,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -16,10 +16,13 @@ authentication_backend:
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com
expiration: 3600 # 1 hour cookies:
inactivity: 300 # 5 minutes - domain: example.com
remember_me_duration: 1y expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me: 1y
# We use redis here to keep the users authenticated when Authelia restarts # We use redis here to keep the users authenticated when Authelia restarts
# It eases development. # It eases development.
redis: redis:

View File

@ -19,7 +19,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
# We use redis here to keep the users authenticated when Authelia restarts # We use redis here to keep the users authenticated when Authelia restarts
# It eases development. # It eases development.
redis: redis:

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -21,10 +21,12 @@ authentication_backend:
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com cookies:
inactivity: 5 - name: authelia_session
expiration: 8 domain: example.com
remember_me_duration: 1y inactivity: 5
expiration: 8
remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -25,9 +25,9 @@ authentication_backend:
session: session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600
inactivity: 300 # 5 minutes inactivity: 300
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key
@ -81,6 +81,7 @@ access_control:
subject: "user:bob" subject: "user:bob"
policy: two_factor policy: two_factor
regulation: regulation:
# Set it to 0 to disable max_retries. # Set it to 0 to disable max_retries.
max_retries: 3 max_retries: 3

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
storage: storage:
encryption_key: a_not_so_secure_encryption_key encryption_key: a_not_so_secure_encryption_key

View File

@ -24,7 +24,7 @@ session:
domain: example.com domain: example.com
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
redis: redis:
host: redis host: redis
port: 6379 port: 6379

View File

@ -45,14 +45,14 @@ click:
} }
// Login 1FA. // Login 1FA.
func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, targetURL string) { func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, domain string, targetURL string) {
rs.doVisitLoginPage(t, page, targetURL) rs.doVisitLoginPage(t, page, domain, targetURL)
rs.doFillLoginPageAndClick(t, page, username, password, keepMeLoggedIn) rs.doFillLoginPageAndClick(t, page, username, password, keepMeLoggedIn)
} }
// Login 1FA and 2FA subsequently (must already be registered). // 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) { 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.verifyIsSecondFactorPage(t, page)
rs.doValidateTOTP(t, page, otpSecret) rs.doValidateTOTP(t, page, otpSecret)
// timeout when targetURL is not defined to prevent a show stopping redirect when visiting a protected domain. // 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. // Login 1FA and register 2FA.
func (rs *RodSession) doLoginAndRegisterTOTP(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool) string { 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) secret := rs.doRegisterTOTP(t, page)
rs.doVisit(t, page, GetLoginBaseURL()) rs.doVisit(t, page, GetLoginBaseURL(BaseDomain))
rs.verifyIsSecondFactorPage(t, page) rs.verifyIsSecondFactorPage(t, page)
return secret return secret

View File

@ -9,12 +9,12 @@ import (
) )
func (rs *RodSession) doLogout(t *testing.T, page *rod.Page) { 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) rs.verifyIsFirstFactorPage(t, page)
} }
func (rs *RodSession) doLogoutWithRedirect(t *testing.T, page *rod.Page, targetURL string, firstFactor bool) { 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 { if firstFactor {
rs.verifyIsFirstFactorPage(t, page) rs.verifyIsFirstFactorPage(t, page)

View File

@ -26,11 +26,11 @@ func (rs *RodSession) doVisitAndVerifyOneFactorStep(t *testing.T, page *rod.Page
rs.verifyIsFirstFactorPage(t, 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 := "" suffix := ""
if targetURL != "" { if targetURL != "" {
suffix = fmt.Sprintf("?rd=%s", 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))
} }

View File

@ -8,16 +8,38 @@ import (
) )
// BaseDomain the base domain. // 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. // PathPrefix the prefix/url_base of the login portal.
var PathPrefix = os.Getenv("PathPrefix") 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. // 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. // 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. // AdminBaseURL the base URL of the admin domain.
var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain) var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain)

View File

@ -1,7 +1,6 @@
package suites package suites
import ( import (
"fmt"
"os" "os"
"strings" "strings"
"time" "time"
@ -22,7 +21,6 @@ func waitUntilServiceLogDetected(
err := utils.CheckUntil(5*time.Second, 1*time.Minute, func() (bool, error) { err := utils.CheckUntil(5*time.Second, 1*time.Minute, func() (bool, error) {
logs, err := dockerEnvironment.Logs(service, []string{"--tail", "20"}) logs, err := dockerEnvironment.Logs(service, []string{"--tail", "20"})
fmt.Printf(".")
if err != nil { if err != nil {
return false, err return false, err
@ -35,8 +33,6 @@ func waitUntilServiceLogDetected(
return false, nil return false, nil
}) })
fmt.Print("\n")
return err return err
} }
@ -55,7 +51,7 @@ func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) erro
90*time.Second, 90*time.Second,
dockerEnvironment, dockerEnvironment,
"authelia-frontend", "authelia-frontend",
[]string{"dev server running at", "ready in"}) []string{"dev server running at", "ready in", "server restarted"})
} }
func waitUntilK3DIsReady(dockerEnvironment *DockerEnvironment) error { func waitUntilK3DIsReady(dockerEnvironment *DockerEnvironment) error {

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -1,9 +1,24 @@
<!DOCTYPE> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Home page</title> <title>Home page</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body> <body>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,7 +3,22 @@
<head> <head>
<title>Public resource</title> <title>Public resource</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
</head> <script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head>
<body> <body>
<h1>Public resource</h1> <h1>Public resource</h1>
<p>This is a public resource.<br/> <p>This is a public resource.<br/>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,7 +3,22 @@
<head> <head>
<title>Public resource</title> <title>Public resource</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
</head> <script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head>
<body> <body>
<h1>Public resource</h1> <h1>Public resource</h1>
<p>This is a public resource.<br/> <p>This is a public resource.<br/>

View File

@ -3,6 +3,21 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +25,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -3,6 +3,22 @@
<head> <head>
<title>Secret</title> <title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
<script>
window.onload = () =>{
/**
* this section renames example.com for the real hostname
* it's required for multi cookie domain suite
* */
const hostname = window.location.hostname
const protocol = window.location.protocol
const port = window.location.port
const domain = hostname.replace(/^\w+\.(.+)$/i, "$1")
const newDomain = `${domain}:${port}`
document.body.innerHTML = document.body.innerHTML.replace(/example.com:8080/ig, newDomain)
}
</script>
</head> </head>
<body id="secret"> <body id="secret">
@ -10,4 +26,4 @@
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>
</html> </html>

View File

@ -9,43 +9,42 @@ http {
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/home; root /usr/share/nginx/html/home;
server_name home.example.com; server_name ~^home\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/public; root /usr/share/nginx/html/public;
server_name public.example.com; server_name ~^public\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/secure; root /usr/share/nginx/html/secure;
server_name secure.example.com; server_name ~^secure\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/admin; root /usr/share/nginx/html/admin;
server_name admin.example.com; server_name ~^admin\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/dev; root /usr/share/nginx/html/dev;
server_name dev.example.com; server_name ~^dev\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/mail; root /usr/share/nginx/html/mail;
server_name mx1.mail.example.com mx2.mail.example.com; server_name ~^(mx[1-2])\.mail\.example([0-9])*\.com$;
} }
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/singlefactor; root /usr/share/nginx/html/singlefactor;
server_name singlefactor.example.com; server_name ~^singlefactor\.example([0-9])*\.com$;
} }
} }

View File

@ -2,7 +2,6 @@
# You can find a documented example of configuration in ./docs/proxies/nginx.md. # You can find a documented example of configuration in ./docs/proxies/nginx.md.
# #
worker_processes 1; worker_processes 1;
events { events {
worker_connections 1024; worker_connections 1024;
} }
@ -10,7 +9,7 @@ events {
http { http {
server { server {
listen 8080 ssl; listen 8080 ssl;
server_name login.example.com; server_name ~^login\.example([0-9])*\.com$;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $frontend_endpoint http://authelia-frontend:3000; set $frontend_endpoint http://authelia-frontend:3000;
@ -114,12 +113,17 @@ http {
proxy_pass $frontend_endpoint; proxy_pass $frontend_endpoint;
} }
# Proxies requests to backend for dev workflow.
location /devworkflow {
proxy_pass $backend_endpoint;
}
} }
# Serves the home page. # Serves the home page.
server { server {
listen 8080 ssl; listen 8080 ssl;
server_name home.example.com; server_name ~^home\.example([0-9])*\.com$;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
@ -141,12 +145,7 @@ http {
# Example configuration of domains protected by Authelia. # Example configuration of domains protected by Authelia.
server { server {
listen 8080 ssl; listen 8080 ssl;
server_name public.example.com server_name ~^(public|admin|secure|dev|singlefactor|mx[1-2])(\.mail)?\.(?<basedomain>example([0-9])*\.com)$;
admin.example.com
secure.example.com
dev.example.com
singlefactor.example.com
mx1.mail.example.com mx2.mail.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify https://authelia-backend:9091/api/verify; set $upstream_verify https://authelia-backend:9091/api/verify;
@ -194,7 +193,7 @@ http {
# Set the `target_url` variable based on the request. It will be used to build the portal # Set the `target_url` variable based on the request. It will be used to build the portal
# URL with the correct redirection parameter. # URL with the correct redirection parameter.
set $target_url $scheme://$http_host$request_uri; set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://login.example.com:8080/?rd=$target_url; error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -245,7 +244,7 @@ http {
proxy_set_header Remote-Email $email; proxy_set_header Remote-Email $email;
set $target_url $scheme://$http_host$request_uri; set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://login.example.com:8080/?rd=$target_url; error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url;
proxy_pass $upstream_headers; proxy_pass $upstream_headers;
} }
@ -254,8 +253,7 @@ http {
# Example configuration of domains protected by Authelia. # Example configuration of domains protected by Authelia.
server { server {
listen 8080 ssl; listen 8080 ssl;
server_name oidc.example.com server_name ~^oidc(-public)?\.(?<basedomain>example([0-9])*\.com)$;
oidc-public.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify https://authelia-backend:9091/api/verify; set $upstream_verify https://authelia-backend:9091/api/verify;
@ -284,7 +282,7 @@ http {
# Set the `target_url` variable based on the request. It will be used to build the portal # Set the `target_url` variable based on the request. It will be used to build the portal
# URL with the correct redirection parameter. # URL with the correct redirection parameter.
set $target_url $scheme://$http_host$request_uri; set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://login.example.com:8080/?rd=$target_url; error_page 401 =302 https://login.$basedomain:8080/?rd=$target_url;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -322,7 +320,7 @@ http {
# Fake Web Mail used to receive emails sent by Authelia. # Fake Web Mail used to receive emails sent by Authelia.
server { server {
listen 8080 ssl; listen 8080 ssl;
server_name mail.example.com; server_name ~^mail\.example([0-9])*\.com$;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://smtp:1080; set $upstream_endpoint http://smtp:1080;
@ -344,7 +342,7 @@ http {
# Fake API emulating Duo behavior # Fake API emulating Duo behavior
server { server {
listen 443 ssl; listen 443 ssl;
server_name duo.example.com; server_name ~^duo\.example([0-9])*\.com$;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://duo-api:3000; set $upstream_endpoint http://duo-api:3000;

View File

@ -86,7 +86,7 @@ access_control:
session: session:
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me: 1y
domain: example.com domain: example.com
redis: redis:
host: redis-service host: redis-service

View File

@ -56,7 +56,7 @@ func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
s.collectScreenshot(ctx.Err(), s.Page) s.collectScreenshot(ctx.Err(), s.Page)
}() }()
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, BaseDomain, "")
methodsButton := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "methods-button") methodsButton := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "methods-button")
err := methodsButton.Click("left", 1) err := methodsButton.Click("left", 1)

Some files were not shown because too many files have changed in this diff Show More