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
parent
ad1a8042fd
commit
8b29cf7ee8
|
@ -7,7 +7,7 @@ trim_trailing_whitespace = true
|
|||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{sh,yml,yaml}]
|
||||
[*.{html,sh,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ const (
|
|||
|
||||
type labelPriority int
|
||||
|
||||
//nolint:deadcode // Kept for future use.
|
||||
//nolint:deadcode,varcheck // Kept for future use.
|
||||
const (
|
||||
labelPriorityCritical labelPriority = iota
|
||||
labelPriorityHigh
|
||||
|
@ -122,7 +122,7 @@ func (s labelStatus) String() string {
|
|||
|
||||
type labelType int
|
||||
|
||||
//nolint:deadcode // Kept for future use.
|
||||
//nolint:deadcode,varcheck // Kept for future use.
|
||||
const (
|
||||
labelTypeFeature labelType = iota
|
||||
labelTypeBugUnconfirmed
|
||||
|
|
|
@ -114,6 +114,30 @@ var hostEntries = []HostEntry{
|
|||
{Domain: "redis-sentinel-0.example.com", IP: "192.168.240.120"},
|
||||
{Domain: "redis-sentinel-1.example.com", IP: "192.168.240.121"},
|
||||
{Domain: "redis-sentinel-2.example.com", IP: "192.168.240.122"},
|
||||
|
||||
// For multi cookie domain tests.
|
||||
{Domain: "login.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "admin.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "singlefactor.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "dev.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "home.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "mx1.mail.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "mx2.mail.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "public.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "secure.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "mail.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "duo.example2.com", IP: "192.168.240.100"},
|
||||
{Domain: "login.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "admin.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "singlefactor.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "dev.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "home.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "mx1.mail.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "mx2.mail.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "public.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "secure.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "mail.example3.com", IP: "192.168.240.100"},
|
||||
{Domain: "duo.example3.com", IP: "192.168.240.100"},
|
||||
}
|
||||
|
||||
func runCommand(cmd string, args ...string) {
|
||||
|
|
|
@ -662,38 +662,76 @@ access_control:
|
|||
## The session cookies identify the user once logged in.
|
||||
## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined.
|
||||
session:
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/c/secrets
|
||||
secret: 'insecure_session_secret'
|
||||
|
||||
## 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
|
||||
# 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
|
||||
## 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
|
||||
# same_site: 'lax'
|
||||
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/c/secrets
|
||||
secret: insecure_session_secret
|
||||
|
||||
## The value for expiration, inactivity, and remember_me_duration are in seconds or the duration notation format.
|
||||
## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format.
|
||||
## See: https://www.authelia.com/c/common#duration-notation-format
|
||||
## All three of these values affect the cookie/session validity period. Longer periods are considered less secure
|
||||
## because a stolen cookie will last longer giving attackers more time to spy or attack.
|
||||
|
||||
## The time before the cookie expires and the session is destroyed if remember me IS NOT selected.
|
||||
expiration: 1h
|
||||
|
||||
## 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
|
||||
## 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 cookie expires and the session is destroyed if remember me IS selected.
|
||||
## Value of -1 disables remember me.
|
||||
remember_me_duration: 1M
|
||||
## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the
|
||||
## user.
|
||||
# expiration: '1h'
|
||||
|
||||
## The time before the cookie expires and the session is destroyed if remember me IS selected by the user. Setting
|
||||
## this value to -1 disables remember me for this session cookie domain.
|
||||
# remember_me: '1M'
|
||||
|
||||
## Cookie Session Domain default 'name' value. The name of the session cookie.
|
||||
name: 'authelia_session'
|
||||
|
||||
## Cookie Session Domain default 'same_site' value. Sets the Cookie SameSite value. Possible options are none, lax,
|
||||
## or strict. Please read https://www.authelia.com/c/session#same_site
|
||||
same_site: 'lax'
|
||||
|
||||
## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format.
|
||||
## See: https://www.authelia.com/c/common#duration-notation-format
|
||||
## All three of these values affect the cookie/session validity period. Longer periods are considered less secure
|
||||
## because a stolen cookie will last longer giving attackers more time to spy or attack.
|
||||
|
||||
## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is
|
||||
## set to 1h, and this is set to 5m, if the user does not select the remember me option their session will get
|
||||
## destroyed after 1h, or after 5m since the last time Authelia detected user activity.
|
||||
inactivity: '5m'
|
||||
|
||||
## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is
|
||||
## destroyed if remember me IS NOT selected by the user.
|
||||
expiration: '1h'
|
||||
|
||||
## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed
|
||||
## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie
|
||||
## domains which do not have a specific 'remember_me' value.
|
||||
remember_me: '1M'
|
||||
|
||||
##
|
||||
## Redis Provider
|
||||
|
|
|
@ -22,7 +22,7 @@ describes the implementation of this. You can use this implementation in various
|
|||
* session:
|
||||
* expiration
|
||||
* inactivity
|
||||
* remember_me_duration
|
||||
* remember_me
|
||||
* regulation:
|
||||
* ban_time
|
||||
* find_time
|
||||
|
|
|
@ -25,13 +25,21 @@ authenticated user and can then order the reverse proxy to let the request pass
|
|||
|
||||
```yaml
|
||||
session:
|
||||
secret: insecure_session_secret
|
||||
|
||||
name: authelia_session
|
||||
same_site: lax
|
||||
inactivity: 5m
|
||||
expiration: 1h
|
||||
remember_me: 1M
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com
|
||||
same_site: lax
|
||||
secret: unsecure_session_secret
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
remember_me_duration: 1M
|
||||
expiration: 1h
|
||||
remember_me: 1d
|
||||
```
|
||||
|
||||
## Providers
|
||||
|
@ -50,34 +58,6 @@ providers are recommended.
|
|||
|
||||
## Options
|
||||
|
||||
### name
|
||||
|
||||
{{< confkey type="string" default="authelia_session" required="no" >}}
|
||||
|
||||
The name of the session cookie. By default this is set to authelia_session. It's mostly useful to change this if you are
|
||||
doing development or running multiple instances of Authelia.
|
||||
|
||||
### domain
|
||||
|
||||
{{< confkey type="string" required="yes" >}}
|
||||
|
||||
The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root
|
||||
of the domain. For example if listening on auth.example.com the cookie should be auth.example.com or example.com.
|
||||
|
||||
### same_site
|
||||
|
||||
{{< confkey type="string" default="lax" required="no" >}}
|
||||
|
||||
Sets the cookies SameSite value. Prior to offering the configuration choice this defaulted to None. The new default is
|
||||
Lax. This option is defined in lower-case. So for example if you want to set it to `Strict`, the value in configuration
|
||||
needs to be `strict`.
|
||||
|
||||
You can read about the SameSite cookie in detail on the
|
||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). In short setting SameSite to Lax
|
||||
is generally the most desirable option for Authelia. None is not recommended unless you absolutely know what you're
|
||||
doing and trust all the protected apps. Strict is not going to work in many use cases and we have not tested it in this
|
||||
state but it's available as an option anyway.
|
||||
|
||||
### secret
|
||||
|
||||
{{< confkey type="string" required="yes" >}}
|
||||
|
@ -91,15 +71,29 @@ It's __strongly recommended__ this is a
|
|||
[Random Alphanumeric String](../../reference/guides/generating-secure-values.md#generating-a-random-alphanumeric-string) with 64 or more
|
||||
characters.
|
||||
|
||||
### expiration
|
||||
### domain
|
||||
|
||||
{{< confkey type="duration" default="1h" required="no" >}}
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
_**Deprecation Notice:** This option is deprecated. See the [cookies](#cookies) section instead._
|
||||
|
||||
The period of time before the cookie expires and the session is destroyed. This is overriden by
|
||||
[remember_me_duration](#remembermeduration) when the remember me box is checked.
|
||||
The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root
|
||||
of the domain. For example if listening on auth.example.com the cookie should be auth.example.com or example.com.
|
||||
|
||||
This value automatically maps to a single cookies configuration using the default values. It cannot be assigned at the
|
||||
same time as a `cookies` configuration.
|
||||
|
||||
### name
|
||||
|
||||
{{< confkey type="string" default="authelia_session" required="no" >}}
|
||||
|
||||
The default `name` value for all [cookies](#cookies) configurations.
|
||||
|
||||
### same_site
|
||||
|
||||
{{< confkey type="string" default="lax" required="no" >}}
|
||||
|
||||
The default `same_site` value for all `cookies` configurations.
|
||||
|
||||
### inactivity
|
||||
|
||||
|
@ -108,18 +102,119 @@ The period of time before the cookie expires and the session is destroyed. This
|
|||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The period of time the user can be inactive for until the session is destroyed. Useful if you want long session timers
|
||||
but don't want unused devices to be vulnerable.
|
||||
The default `inactivity` value for all [cookies](#cookies) configurations.
|
||||
|
||||
### remember_me_duration
|
||||
### expiration
|
||||
|
||||
{{< confkey type="duration" default="1h" required="no" >}}
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The default `expiration` value for all [cookies](#cookies) configurations.
|
||||
|
||||
### remember_me
|
||||
|
||||
{{< confkey type="duration" default="1M" required="no" >}}
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The default `remember_me` value for all [cookies](#cookies) configurations.
|
||||
|
||||
### cookies
|
||||
|
||||
The list of specific cookie domains that Authelia is configured to handle. Domains not properly configured will
|
||||
automatically be denied by Authelia. The list allows administrators to define multiple session cookie domain
|
||||
configurations with individual settings.
|
||||
|
||||
#### name
|
||||
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
*__Default Value:__ This option takes its default value from the [name](#name) setting above.*
|
||||
|
||||
The name of the session cookie. By default this is set to the `name` value in the main session configuration section.
|
||||
|
||||
#### domain
|
||||
|
||||
{{< confkey type="string" required="yes" >}}
|
||||
|
||||
The domain the cookie is assigned to protect. This must be the same as the domain Authelia is served on or the root
|
||||
of the domain, and consequently if the [authelia_url](#authelia_url) is configured must be able to read and write cookies
|
||||
for the domain. For example if listening on `auth.example.com` the cookie should be either `auth.example.com` or
|
||||
`example.com`.
|
||||
|
||||
Please note most good DynamicDNS solutions fall into a specially protected group of domains and browsers do not allow
|
||||
you to write cookies for the root domain. i.e. if you have been assigned `john.duckdns.org` you can't use `duckdns.org`
|
||||
for the domain value as browsers will not allow `john.duckdns.org` to read or write cookies for `duckdns.org`.
|
||||
|
||||
Consequently, if you have `john.duckdns.org` and `mary.duckdns.org` you cannot share cookies between these domains.
|
||||
|
||||
#### authelia_url
|
||||
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
*__Note:__ The AuthRequest implementation does not support redirection control on the authorization server. This means
|
||||
that the `authelia_url` option is ineffectual for both NGINX and HAProxy, or any other proxy which uses the AuthRequest
|
||||
implementation.*
|
||||
|
||||
This is a completely optional URL which is the root URL of your Authelia installation for this cookie domain which can
|
||||
be used to generate the appropriate redirection for proxies which support this.
|
||||
|
||||
If this option is absent you must use the appropriate query parameter or header for your relevant proxy.
|
||||
|
||||
#### same_site
|
||||
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
*__Default Value:__ This option takes its default value from the [same_site](#samesite) setting above.*
|
||||
|
||||
Sets the cookies SameSite value. Prior to offering the configuration choice this defaulted to None. The new default is
|
||||
Lax. This option is defined in lower-case. So for example if you want to set it to `Strict`, the value in configuration
|
||||
needs to be `strict`.
|
||||
|
||||
You can read about the SameSite cookie in detail on the
|
||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). In short setting SameSite to Lax
|
||||
is generally the most desirable option for Authelia. None is not recommended unless you absolutely know what you're
|
||||
doing and trust all the protected apps. Strict is not going to work in many use cases and we have not tested it in this
|
||||
state but it's available as an option anyway.
|
||||
|
||||
#### inactivity
|
||||
|
||||
{{< confkey type="duration" required="no" >}}
|
||||
|
||||
*__Default Value:__ This option takes its default value from the [inactivity](#inactivity) setting above.*
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The period of time the user can be inactive for until the session is destroyed. Useful if you want long session timers
|
||||
but don't want unused devices to be vulnerable.
|
||||
|
||||
#### expiration
|
||||
|
||||
{{< confkey type="duration" required="no" >}}
|
||||
|
||||
*__Default Value:__ This option takes its default value from the [expiration](#expiration) setting above.*
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The period of time before the cookie expires and the session is destroyed. This is overriden by
|
||||
[remember_me](#rememberme) when the remember me box is checked.
|
||||
|
||||
#### remember_me
|
||||
|
||||
{{< confkey type="duration" required="no" >}}
|
||||
|
||||
*__Default Value:__ This option takes its default value from the [remember_me](#rememberme) setting above.*
|
||||
|
||||
*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see
|
||||
the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.*
|
||||
|
||||
The period of time before the cookie expires and the session is destroyed when the remember me box is checked. Setting
|
||||
this to `-1` disables this feature entirely.
|
||||
this to `-1` disables this feature entirely for this session cookie domain.
|
||||
|
||||
## Security
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ for, and the structure it must have.
|
|||
│ └─⫸ Commit Scope: api|autheliabot|authentication|authorization|buildkite|bundler|cmd|
|
||||
│ codecov|commands|configuration|deps|docker|duo|go|golangci-lint|
|
||||
│ handlers|logging|metrics|middlewares|mocks|model|notification|npm|ntp|
|
||||
│ oidc|regulation|renovate|reviewdog|server|session|storage|suites|
|
||||
│ templates|totp|utils|web
|
||||
│ oidc|random|regulation|renovate|reviewdog|server|session|storage|
|
||||
│ suites|templates|totp|utils|web
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|i18n|perf|refactor|release|revert|test
|
||||
```
|
||||
|
@ -93,6 +93,7 @@ commit messages).
|
|||
* notification
|
||||
* ntp
|
||||
* oidc
|
||||
* random
|
||||
* regulation
|
||||
* server
|
||||
* session
|
||||
|
|
|
@ -256,7 +256,7 @@ database. The value of this option should be long and as random as possible. See
|
|||
[documentation](../../configuration/session/introduction.md#secret) for this option.
|
||||
|
||||
The validity period of session is highly configurable. For example in a highly security conscious domain you could
|
||||
set the session [remember_me_duration](../../configuration/session/introduction.md#remembermeduration) to 0 to disable this
|
||||
set the session [remember_me](../../configuration/session/introduction.md#rememberme) to 0 to disable this
|
||||
feature, and set the [expiration](../../configuration/session/introduction.md#expiration) to 2 hours and the
|
||||
[inactivity](../../configuration/session/introduction.md#inactivity) of 10 minutes. Configuring the session security in this
|
||||
manner would mean if the cookie age was more than 2 hours or if the user was inactive for more than 10 minutes the
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -39,12 +39,14 @@ access_control:
|
|||
policy: two_factor
|
||||
|
||||
session:
|
||||
name: authelia_session
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
|
||||
secret: unsecure_session_secret
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
|
|
|
@ -31,11 +31,13 @@ access_control:
|
|||
policy: two_factor
|
||||
|
||||
session:
|
||||
name: authelia_session
|
||||
secret: unsecure_session_secret
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
|
|
|
@ -662,38 +662,76 @@ access_control:
|
|||
## The session cookies identify the user once logged in.
|
||||
## The available providers are: `memory`, `redis`. Memory is the provider unless redis is defined.
|
||||
session:
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/c/secrets
|
||||
secret: 'insecure_session_secret'
|
||||
|
||||
## 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
|
||||
# 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
|
||||
## 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
|
||||
# same_site: 'lax'
|
||||
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/c/secrets
|
||||
secret: insecure_session_secret
|
||||
|
||||
## The value for expiration, inactivity, and remember_me_duration are in seconds or the duration notation format.
|
||||
## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format.
|
||||
## See: https://www.authelia.com/c/common#duration-notation-format
|
||||
## All three of these values affect the cookie/session validity period. Longer periods are considered less secure
|
||||
## because a stolen cookie will last longer giving attackers more time to spy or attack.
|
||||
|
||||
## The time before the cookie expires and the session is destroyed if remember me IS NOT selected.
|
||||
expiration: 1h
|
||||
|
||||
## 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
|
||||
## 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 cookie expires and the session is destroyed if remember me IS selected.
|
||||
## Value of -1 disables remember me.
|
||||
remember_me_duration: 1M
|
||||
## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the
|
||||
## user.
|
||||
# expiration: '1h'
|
||||
|
||||
## The time before the cookie expires and the session is destroyed if remember me IS selected by the user. Setting
|
||||
## this value to -1 disables remember me for this session cookie domain.
|
||||
# remember_me: '1M'
|
||||
|
||||
## Cookie Session Domain default 'name' value. The name of the session cookie.
|
||||
name: 'authelia_session'
|
||||
|
||||
## Cookie Session Domain default 'same_site' value. Sets the Cookie SameSite value. Possible options are none, lax,
|
||||
## or strict. Please read https://www.authelia.com/c/session#same_site
|
||||
same_site: 'lax'
|
||||
|
||||
## The value for inactivity, expiration, and remember_me are in seconds or the duration notation format.
|
||||
## See: https://www.authelia.com/c/common#duration-notation-format
|
||||
## All three of these values affect the cookie/session validity period. Longer periods are considered less secure
|
||||
## because a stolen cookie will last longer giving attackers more time to spy or attack.
|
||||
|
||||
## Cookie Session Domain default 'inactivity' value. The inactivity time before the session is reset. If expiration is
|
||||
## set to 1h, and this is set to 5m, if the user does not select the remember me option their session will get
|
||||
## destroyed after 1h, or after 5m since the last time Authelia detected user activity.
|
||||
inactivity: '5m'
|
||||
|
||||
## Cookie Session Domain default 'expiration' value. The time before the session cookie expires and the session is
|
||||
## destroyed if remember me IS NOT selected by the user.
|
||||
expiration: '1h'
|
||||
|
||||
## Cookie Session Domain default 'remember_me' value. The time before the cookie expires and the session is destroyed
|
||||
## if remember me IS selected by the user. Setting this value to -1 disables remember me for all session cookie
|
||||
## domains which do not have a specific 'remember_me' value.
|
||||
remember_me: '1M'
|
||||
|
||||
##
|
||||
## Redis Provider
|
||||
|
|
|
@ -134,4 +134,11 @@ var deprecations = map[string]Deprecation{
|
|||
AutoMap: true,
|
||||
MapFunc: nil,
|
||||
},
|
||||
"session.remember_me_duration": {
|
||||
Version: model.SemanticVersion{Major: 4, Minor: 38},
|
||||
Key: "session.remember_me_duration",
|
||||
NewKey: "session.remember_me",
|
||||
AutoMap: true,
|
||||
MapFunc: nil,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -323,7 +323,7 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "Admin", config.Notifier.SMTP.Sender.Name)
|
||||
assert.Equal(t, "admin@example.com", config.Notifier.SMTP.Sender.Address)
|
||||
assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMeDuration)
|
||||
assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMe)
|
||||
}
|
||||
|
||||
func TestShouldParseRegex(t *testing.T) {
|
||||
|
|
|
@ -105,13 +105,23 @@ var Keys = []string{
|
|||
"authentication_backend.ldap.permit_feature_detection_failure",
|
||||
"authentication_backend.ldap.user",
|
||||
"authentication_backend.ldap.password",
|
||||
"session.secret",
|
||||
"session.name",
|
||||
"session.domain",
|
||||
"session.same_site",
|
||||
"session.secret",
|
||||
"session.expiration",
|
||||
"session.inactivity",
|
||||
"session.remember_me_duration",
|
||||
"session.remember_me",
|
||||
"session",
|
||||
"session.cookies",
|
||||
"session.cookies[].name",
|
||||
"session.cookies[].domain",
|
||||
"session.cookies[].same_site",
|
||||
"session.cookies[].expiration",
|
||||
"session.cookies[].inactivity",
|
||||
"session.cookies[].remember_me",
|
||||
"session.cookies[]",
|
||||
"session.cookies[].authelia_url",
|
||||
"session.redis.host",
|
||||
"session.redis.port",
|
||||
"session.redis.username",
|
||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -36,24 +37,42 @@ type RedisSessionConfiguration struct {
|
|||
|
||||
// SessionConfiguration represents the configuration related to user sessions.
|
||||
type SessionConfiguration struct {
|
||||
Name string `koanf:"name"`
|
||||
Domain string `koanf:"domain"`
|
||||
SameSite string `koanf:"same_site"`
|
||||
Secret string `koanf:"secret"`
|
||||
Expiration time.Duration `koanf:"expiration"`
|
||||
Inactivity time.Duration `koanf:"inactivity"`
|
||||
RememberMeDuration time.Duration `koanf:"remember_me_duration"`
|
||||
|
||||
SessionCookieCommonConfiguration `koanf:",squash"`
|
||||
|
||||
Cookies []SessionCookieConfiguration `koanf:"cookies"`
|
||||
|
||||
Redis *RedisSessionConfiguration `koanf:"redis"`
|
||||
}
|
||||
|
||||
type SessionCookieCommonConfiguration struct {
|
||||
Name string `koanf:"name"`
|
||||
Domain string `koanf:"domain"`
|
||||
SameSite string `koanf:"same_site"`
|
||||
Expiration time.Duration `koanf:"expiration"`
|
||||
Inactivity time.Duration `koanf:"inactivity"`
|
||||
RememberMe time.Duration `koanf:"remember_me"`
|
||||
|
||||
DisableRememberMe bool
|
||||
}
|
||||
|
||||
// SessionCookieConfiguration represents the configuration for a cookie domain.
|
||||
type SessionCookieConfiguration struct {
|
||||
SessionCookieCommonConfiguration `koanf:",squash"`
|
||||
|
||||
AutheliaURL *url.URL `koanf:"authelia_url"`
|
||||
}
|
||||
|
||||
// DefaultSessionConfiguration is the default session configuration.
|
||||
var DefaultSessionConfiguration = SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session",
|
||||
Expiration: time.Hour,
|
||||
Inactivity: time.Minute * 5,
|
||||
RememberMeDuration: time.Hour * 24 * 30,
|
||||
RememberMe: time.Hour * 24 * 30,
|
||||
SameSite: "lax",
|
||||
},
|
||||
}
|
||||
|
||||
// DefaultRedisConfiguration is the default redis configuration.
|
||||
|
|
|
@ -97,7 +97,7 @@ session:
|
|||
name: authelia_session
|
||||
expiration: 3600000 # 1 hour
|
||||
inactivity: 300000 # 5 minutes
|
||||
remember_me_duration: -1
|
||||
remember_me: -1
|
||||
domain: example.com
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
|
|
|
@ -25,9 +25,15 @@ func newDefaultConfig() schema.Configuration {
|
|||
DefaultPolicy: "two_factor",
|
||||
}
|
||||
config.Session = schema.SessionConfiguration{
|
||||
Domain: examplecom,
|
||||
Name: "authelia_session",
|
||||
Secret: "secret",
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session",
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.Storage.EncryptionKey = testEncryptionKey
|
||||
config.Storage.Local = &schema.LocalStorageConfiguration{
|
||||
|
@ -49,6 +55,8 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
|
|||
ValidateConfiguration(&config, validator)
|
||||
require.Len(t, validator.Errors(), 0)
|
||||
|
||||
config = newDefaultConfig()
|
||||
|
||||
config.Notifier.SMTP = nil
|
||||
config.Notifier.FileSystem = nil
|
||||
|
||||
|
@ -135,6 +143,8 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
|
|||
|
||||
assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
|
||||
|
||||
config = newDefaultConfig()
|
||||
|
||||
validator = schema.NewStructValidator()
|
||||
config.CertificatesDirectory = "const.go"
|
||||
|
||||
|
|
|
@ -248,7 +248,7 @@ const (
|
|||
// Session error constants.
|
||||
const (
|
||||
errFmtSessionOptionRequired = "session: option '%s' is required"
|
||||
errFmtSessionDomainMustBeRoot = "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '%s'"
|
||||
errFmtSessionLegacyAndWarning = "session: option 'domain' and option 'cookies' can't be specified at the same time"
|
||||
errFmtSessionSameSite = "session: option 'same_site' must be one of '%s' but is configured as '%s'"
|
||||
errFmtSessionSecretRequired = "session: option 'secret' is required when using the '%s' provider"
|
||||
errFmtSessionRedisPortRange = "session: redis: option 'port' must be between 1 and 65535 but is configured as '%d'"
|
||||
|
@ -258,6 +258,16 @@ const (
|
|||
|
||||
errFmtSessionRedisSentinelMissingName = "session: redis: high_availability: option 'sentinel_name' is required"
|
||||
errFmtSessionRedisSentinelNodeHostMissing = "session: redis: high_availability: option 'nodes': option 'host' is required for each node but one or more nodes are missing this"
|
||||
|
||||
errFmtSessionDomainMustBeRoot = "session: domain config %s: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '%s'"
|
||||
errFmtSessionDomainSameSite = "session: domain config %s: option 'same_site' must be one of '%s' but is configured as '%s'"
|
||||
errFmtSessionDomainRequired = "session: domain config %s: option 'domain' is required"
|
||||
errFmtSessionDomainHasPeriodPrefix = "session: domain config %s: option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it"
|
||||
errFmtSessionDomainDuplicate = "session: domain config %s: option 'domain' is a duplicate value for another configured session domain"
|
||||
errFmtSessionDomainDuplicateCookieScope = "session: domain config %s: option 'domain' shares the same cookie domain scope as another configured session domain"
|
||||
errFmtSessionDomainPortalURLInsecure = "session: domain config %s: option 'authelia_url' does not have a secure scheme with a value of '%s'"
|
||||
errFmtSessionDomainPortalURLNotInCookieScope = "session: domain config %s: option 'authelia_url' does not share a cookie scope with domain '%s' with a value of '%s'"
|
||||
errFmtSessionDomainInvalidDomain = "session: domain config %s: option 'domain' is not a valid domain"
|
||||
)
|
||||
|
||||
// Regulation Error Consts.
|
||||
|
@ -363,7 +373,10 @@ var (
|
|||
validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()}
|
||||
)
|
||||
|
||||
var reKeyReplacer = regexp.MustCompile(`\[\d+]`)
|
||||
var (
|
||||
reKeyReplacer = regexp.MustCompile(`\[\d+]`)
|
||||
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
|
||||
)
|
||||
|
||||
var replacedKeys = map[string]string{
|
||||
"authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify",
|
||||
|
|
|
@ -12,5 +12,5 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
examplecom = "example.com"
|
||||
exampleDotCom = "example.com"
|
||||
)
|
||||
|
|
|
@ -257,7 +257,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
|||
RedirectURIs: []string{
|
||||
"https://google.com",
|
||||
},
|
||||
SectorIdentifier: mustParseURL(examplecom),
|
||||
SectorIdentifier: mustParseURL(exampleDotCom),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -289,12 +289,12 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
|
|||
},
|
||||
},
|
||||
Errors: []string{
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "scheme", "https"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "path", "/path"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "query", "query=abc"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "fragment", "fragment"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "username", "user"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", examplecom, "password"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "scheme", "https"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "path", "/path"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "query", "query=abc"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "fragment", "fragment"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifier, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "username", "user"),
|
||||
fmt.Sprintf(errFmtOIDCClientInvalidSectorIdentifierWithoutValue, "client-invalid-sector", "https://user:pass@example.com/path?query=abc#fragment", exampleDotCom, "password"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -23,7 +23,7 @@ func (suite *NotifierSuite) SetupTest() {
|
|||
Username: "john",
|
||||
Password: "password",
|
||||
Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"},
|
||||
Host: examplecom,
|
||||
Host: exampleDotCom,
|
||||
Port: 25,
|
||||
}
|
||||
suite.config.FileSystem = nil
|
||||
|
@ -78,7 +78,7 @@ func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() {
|
|||
suite.Assert().Len(suite.validator.Warnings(), 0)
|
||||
suite.Assert().Len(suite.validator.Errors(), 0)
|
||||
|
||||
suite.Assert().Equal(examplecom, suite.config.SMTP.TLS.ServerName)
|
||||
suite.Assert().Equal(exampleDotCom, suite.config.SMTP.TLS.ServerName)
|
||||
suite.Assert().Equal(uint16(tls.VersionTLS12), suite.config.SMTP.TLS.MinimumVersion.Value)
|
||||
suite.Assert().False(suite.config.SMTP.TLS.SkipVerify)
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() {
|
|||
}
|
||||
|
||||
func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() {
|
||||
suite.config.SMTP.Host = examplecom
|
||||
suite.config.SMTP.Host = exampleDotCom
|
||||
suite.config.SMTP.TLS = &schema.TLSConfig{
|
||||
MinimumVersion: schema.TLSVersion{Value: tls.VersionSSL30}, //nolint:staticcheck
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnSSL30() {
|
|||
}
|
||||
|
||||
func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() {
|
||||
suite.config.SMTP.Host = examplecom
|
||||
suite.config.SMTP.Host = exampleDotCom
|
||||
suite.config.SMTP.TLS = &schema.TLSConfig{
|
||||
MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13},
|
||||
MaximumVersion: schema.TLSVersion{Value: tls.VersionTLS10},
|
||||
|
@ -140,7 +140,7 @@ func (suite *NotifierSuite) TestSMTPShouldErrorOnTLSMinVerGreaterThanMaxVer() {
|
|||
}
|
||||
|
||||
func (suite *NotifierSuite) TestSMTPShouldWarnOnDisabledSTARTTLS() {
|
||||
suite.config.SMTP.Host = examplecom
|
||||
suite.config.SMTP.Host = exampleDotCom
|
||||
suite.config.SMTP.DisableStartTLS = true
|
||||
|
||||
ValidateNotifier(&suite.config, suite.validator)
|
||||
|
|
|
@ -35,18 +35,11 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru
|
|||
config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min.
|
||||
}
|
||||
|
||||
if config.RememberMeDuration <= 0 && config.RememberMeDuration != schema.RememberMeDisabled {
|
||||
config.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month.
|
||||
}
|
||||
|
||||
if config.Domain == "" {
|
||||
validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "domain"))
|
||||
} else if strings.HasPrefix(config.Domain, ".") {
|
||||
validator.PushWarning(fmt.Errorf("session: option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it"))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(config.Domain, "*.") {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, config.Domain))
|
||||
switch {
|
||||
case config.RememberMe == schema.RememberMeDisabled:
|
||||
config.DisableRememberMe = true
|
||||
case config.RememberMe <= 0:
|
||||
config.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month.
|
||||
}
|
||||
|
||||
if config.SameSite == "" {
|
||||
|
@ -54,6 +47,137 @@ func validateSession(config *schema.SessionConfiguration, validator *schema.Stru
|
|||
} else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
|
||||
validator.Push(fmt.Errorf(errFmtSessionSameSite, strings.Join(validSessionSameSiteValues, "', '"), config.SameSite))
|
||||
}
|
||||
|
||||
cookies := len(config.Cookies)
|
||||
|
||||
switch {
|
||||
case cookies == 0 && config.Domain != "":
|
||||
// Add legacy configuration to the domains list.
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: config.Name,
|
||||
Domain: config.Domain,
|
||||
SameSite: config.SameSite,
|
||||
Expiration: config.Expiration,
|
||||
Inactivity: config.Inactivity,
|
||||
RememberMe: config.RememberMe,
|
||||
DisableRememberMe: config.DisableRememberMe,
|
||||
},
|
||||
})
|
||||
case cookies != 0 && config.Domain != "":
|
||||
validator.Push(fmt.Errorf(errFmtSessionLegacyAndWarning))
|
||||
}
|
||||
|
||||
validateSessionCookieDomains(config, validator)
|
||||
}
|
||||
|
||||
func validateSessionCookieDomains(config *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
if len(config.Cookies) == 0 {
|
||||
validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "domain"))
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
|
||||
for i, d := range config.Cookies {
|
||||
validateSessionDomainName(i, config, validator)
|
||||
|
||||
validateSessionUniqueCookieDomain(i, config, domains, validator)
|
||||
|
||||
validateSessionCookieName(i, config)
|
||||
|
||||
validateSessionSafeRedirection(i, config, validator)
|
||||
|
||||
validateSessionExpiration(i, config)
|
||||
|
||||
validateSessionRememberMe(i, config)
|
||||
|
||||
validateSessionSameSite(i, config, validator)
|
||||
|
||||
domains = append(domains, d.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
// validateSessionDomainName returns error if the domain name is invalid.
|
||||
func validateSessionDomainName(i int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
var d = config.Cookies[i]
|
||||
|
||||
switch {
|
||||
case d.Domain == "":
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainRequired, sessionDomainDescriptor(i, d)))
|
||||
case strings.HasPrefix(d.Domain, "*."):
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain))
|
||||
case strings.HasPrefix(d.Domain, "."):
|
||||
validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d)))
|
||||
case !reDomainCharacters.MatchString(d.Domain):
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, sessionDomainDescriptor(i, d)))
|
||||
}
|
||||
}
|
||||
|
||||
func validateSessionCookieName(i int, config *schema.SessionConfiguration) {
|
||||
if config.Cookies[i].Name == "" {
|
||||
config.Cookies[i].Name = config.Name
|
||||
}
|
||||
}
|
||||
|
||||
func validateSessionExpiration(i int, config *schema.SessionConfiguration) {
|
||||
if config.Cookies[i].Expiration <= 0 {
|
||||
config.Cookies[i].Expiration = config.Expiration
|
||||
}
|
||||
|
||||
if config.Cookies[i].Inactivity <= 0 {
|
||||
config.Cookies[i].Inactivity = config.Inactivity
|
||||
}
|
||||
}
|
||||
|
||||
// validateSessionUniqueCookieDomain Check the current domains do not share a root domain with previous domains.
|
||||
func validateSessionUniqueCookieDomain(i int, config *schema.SessionConfiguration, domains []string, validator *schema.StructValidator) {
|
||||
var d = config.Cookies[i]
|
||||
if utils.IsStringInSliceF(d.Domain, domains, utils.HasDomainSuffix) {
|
||||
if utils.IsStringInSlice(d.Domain, domains) {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(i, d)))
|
||||
} else {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainDuplicateCookieScope, sessionDomainDescriptor(i, d)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateSessionSafeRedirection validates that AutheliaURL is safe for redirection.
|
||||
func validateSessionSafeRedirection(index int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
var d = config.Cookies[index]
|
||||
|
||||
if d.AutheliaURL != nil && d.Domain != "" && !utils.IsURISafeRedirection(d.AutheliaURL, d.Domain) {
|
||||
if utils.IsURISecure(d.AutheliaURL) {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainPortalURLNotInCookieScope, sessionDomainDescriptor(index, d), d.Domain, d.AutheliaURL))
|
||||
} else {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainPortalURLInsecure, sessionDomainDescriptor(index, d), d.AutheliaURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateSessionRememberMe(i int, config *schema.SessionConfiguration) {
|
||||
if config.Cookies[i].RememberMe <= 0 && config.Cookies[i].RememberMe != schema.RememberMeDisabled {
|
||||
config.Cookies[i].RememberMe = config.RememberMe
|
||||
}
|
||||
|
||||
if config.Cookies[i].RememberMe == schema.RememberMeDisabled {
|
||||
config.Cookies[i].DisableRememberMe = true
|
||||
}
|
||||
}
|
||||
|
||||
func validateSessionSameSite(i int, config *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
if config.Cookies[i].SameSite == "" {
|
||||
if utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
|
||||
config.Cookies[i].SameSite = config.SameSite
|
||||
} else {
|
||||
config.Cookies[i].SameSite = schema.DefaultSessionConfiguration.SameSite
|
||||
}
|
||||
} else if !utils.IsStringInSlice(config.Cookies[i].SameSite, validSessionSameSiteValues) {
|
||||
validator.Push(fmt.Errorf(errFmtSessionDomainSameSite, sessionDomainDescriptor(i, config.Cookies[i]), strings.Join(validSessionSameSiteValues, "', '"), config.Cookies[i].SameSite))
|
||||
}
|
||||
}
|
||||
|
||||
func sessionDomainDescriptor(position int, domain schema.SessionCookieConfiguration) string {
|
||||
return fmt.Sprintf("#%d (domain '%s')", position+1, domain.Domain)
|
||||
}
|
||||
|
||||
func validateRedisCommon(config *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
|
|
|
@ -3,7 +3,9 @@ package validator
|
|||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -14,7 +16,8 @@ import (
|
|||
func newDefaultSessionConfig() schema.SessionConfiguration {
|
||||
config := schema.SessionConfiguration{}
|
||||
config.Secret = testJWTSecret
|
||||
config.Domain = examplecom
|
||||
config.Domain = exampleDotCom
|
||||
config.Cookies = []schema.SessionCookieConfiguration{}
|
||||
|
||||
return config
|
||||
}
|
||||
|
@ -30,15 +33,144 @@ func TestShouldSetDefaultSessionValues(t *testing.T) {
|
|||
assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMeDuration, config.RememberMeDuration)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite)
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultSessionDomainsValues(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have schema.SessionConfiguration
|
||||
expected schema.SessionConfiguration
|
||||
errs []string
|
||||
}{
|
||||
{
|
||||
"ShouldSetGoodDefaultValues",
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
},
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", Domain: exampleDotCom, SameSite: "lax", Expiration: time.Hour,
|
||||
Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"ShouldNotSetBadDefaultValues",
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", Domain: exampleDotCom,
|
||||
Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", SameSite: "BAD VALUE", Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", Domain: exampleDotCom, SameSite: schema.DefaultSessionConfiguration.SameSite,
|
||||
Expiration: time.Hour, Inactivity: time.Minute, RememberMe: time.Hour * 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]string{
|
||||
"session: option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'BAD VALUE'",
|
||||
},
|
||||
},
|
||||
{
|
||||
"ShouldSetDefaultValuesForEachConfig",
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute,
|
||||
RememberMe: schema.RememberMeDisabled,
|
||||
},
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
},
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "example2.com", Name: "authelia_session", SameSite: "strict",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schema.SessionConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "default_session", SameSite: "lax", Expiration: time.Hour, Inactivity: time.Minute,
|
||||
RememberMe: schema.RememberMeDisabled, DisableRememberMe: true,
|
||||
},
|
||||
Cookies: []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "default_session", Domain: exampleDotCom, SameSite: "lax",
|
||||
Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session", Domain: "example2.com", SameSite: "strict",
|
||||
Expiration: time.Hour, Inactivity: time.Minute, RememberMe: schema.RememberMeDisabled, DisableRememberMe: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
validator := schema.NewStructValidator()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
validator.Clear()
|
||||
|
||||
have := tc.have
|
||||
|
||||
ValidateSession(&have, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
|
||||
errs := validator.Errors()
|
||||
require.Len(t, validator.Errors(), len(tc.errs))
|
||||
|
||||
for i, err := range errs {
|
||||
assert.EqualError(t, err, tc.errs[i])
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expected, have)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
config.Expiration, config.Inactivity, config.RememberMeDuration = -1, -1, -2
|
||||
config.Expiration, config.Inactivity, config.RememberMe = -1, -1, -2
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
|
@ -46,7 +178,7 @@ func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {
|
|||
assert.Len(t, validator.Errors(), 0)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMeDuration, config.RememberMeDuration)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMe, config.RememberMe)
|
||||
}
|
||||
|
||||
func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) {
|
||||
|
@ -60,7 +192,7 @@ func TestShouldWarnSessionValuesWhenPotentiallyInvalid(t *testing.T) {
|
|||
require.Len(t, validator.Warnings(), 1)
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
|
||||
assert.EqualError(t, validator.Warnings()[0], "session: option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it")
|
||||
assert.EqualError(t, validator.Warnings()[0], "session: domain config #1 (domain '.example.com'): option 'domain' has a prefix of '.' which is not supported or intended behaviour: you can use this at your own risk but we recommend removing it")
|
||||
}
|
||||
|
||||
func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
|
||||
|
@ -72,6 +204,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
|
|||
assert.Len(t, validator.Errors(), 0)
|
||||
validator.Clear()
|
||||
|
||||
config = newDefaultSessionConfig()
|
||||
|
||||
// Set redis config because password must be set only when redis is used.
|
||||
config.Redis = &schema.RedisSessionConfiguration{
|
||||
Host: "redis.localhost",
|
||||
|
@ -81,8 +215,8 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
|
|||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.False(t, validator.HasErrors())
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
|
||||
assert.Equal(t, 8, config.Redis.MaximumActiveConnections)
|
||||
}
|
||||
|
@ -98,7 +232,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) {
|
|||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
require.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, -1))
|
||||
|
@ -131,6 +265,9 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {
|
|||
assert.Len(t, validator.Errors(), 0)
|
||||
validator.Clear()
|
||||
|
||||
config = newDefaultSessionConfig()
|
||||
config.Secret = ""
|
||||
|
||||
// Set redis config because password must be set only when redis is used.
|
||||
config.Redis = &schema.RedisSessionConfiguration{
|
||||
Host: "redis.localhost",
|
||||
|
@ -153,6 +290,8 @@ func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) {
|
|||
assert.Len(t, validator.Errors(), 0)
|
||||
validator.Clear()
|
||||
|
||||
config = newDefaultSessionConfig()
|
||||
|
||||
// Set redis config because password must be set only when redis is used.
|
||||
config.Redis = &schema.RedisSessionConfiguration{
|
||||
Host: "redis.localhost",
|
||||
|
@ -287,7 +426,24 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi
|
|||
|
||||
validator.Clear()
|
||||
|
||||
config.Redis.Port = -1
|
||||
config = newDefaultSessionConfig()
|
||||
|
||||
config.Secret = ""
|
||||
config.Redis = &schema.RedisSessionConfiguration{
|
||||
Port: -1,
|
||||
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
|
||||
SentinelName: "sentinel",
|
||||
SentinelPassword: "abc123",
|
||||
Nodes: []schema.RedisNode{
|
||||
{
|
||||
Host: "node1",
|
||||
Port: 26379,
|
||||
},
|
||||
},
|
||||
RouteByLatency: true,
|
||||
RouteRandomly: true,
|
||||
},
|
||||
}
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
|
@ -434,6 +590,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
|
|||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
config.Cookies = []schema.SessionCookieConfiguration{}
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
|
@ -449,9 +606,141 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
|
|||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain '*.example.com'): option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenDomainNameIsInvalid(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = "example!.com"
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "session: domain config #1 (domain 'example!.com'): option 'domain' is not a valid domain")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenHaveDuplicatedDomainName(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
AutheliaURL: MustParseURL("https://login.example.com"),
|
||||
})
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
AutheliaURL: MustParseURL("https://login.example.com"),
|
||||
})
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'")
|
||||
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(1, schema.SessionCookieConfiguration{SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{Domain: exampleDotCom}})))
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenSubdomainConflicts(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
AutheliaURL: MustParseURL("https://login.example.com"),
|
||||
})
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "internal.example.com",
|
||||
},
|
||||
AutheliaURL: MustParseURL("https://login.internal.example.com"),
|
||||
})
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "session: domain config #2 (domain 'internal.example.com'): option 'domain' shares the same cookie domain scope as another configured session domain")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenDomainIsInvalid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have string
|
||||
expected []string
|
||||
}{
|
||||
{"ShouldRaiseErrorOnMissingDomain", "", []string{"session: domain config #1 (domain ''): option 'domain' is required"}},
|
||||
{"ShouldNotRaiseErrorOnValidDomain", exampleDotCom, nil},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
|
||||
config.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: tc.have,
|
||||
},
|
||||
AutheliaURL: MustParseURL("https://auth.example.com")},
|
||||
}
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), len(tc.expected))
|
||||
|
||||
for i, expected := range tc.expected {
|
||||
assert.EqualError(t, validator.Errors()[i], expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenPortalURLIsInvalid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have string
|
||||
expected []string
|
||||
}{
|
||||
{"ShouldRaiseErrorOnInvalidScope", "https://example2.com/login", []string{"session: domain config #1 (domain 'example.com'): option 'authelia_url' does not share a cookie scope with domain 'example.com' with a value of 'https://example2.com/login'"}},
|
||||
{"ShouldRaiseErrorOnInvalidScheme", "http://example.com/login", []string{"session: domain config #1 (domain 'example.com'): option 'authelia_url' does not have a secure scheme with a value of 'http://example.com/login'"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
config.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session",
|
||||
Domain: exampleDotCom,
|
||||
},
|
||||
AutheliaURL: MustParseURL(tc.have)},
|
||||
}
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), len(tc.expected))
|
||||
|
||||
for i, expected := range tc.expected {
|
||||
assert.EqualError(t, validator.Errors()[i], expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
|
||||
|
@ -462,22 +751,28 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
|
|||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
require.Len(t, validator.Errors(), 2)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "session: option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'NOne'")
|
||||
assert.EqualError(t, validator.Errors()[1], "session: domain config #1 (domain 'example.com'): option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'NOne'")
|
||||
}
|
||||
|
||||
func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
var config schema.SessionConfiguration
|
||||
|
||||
validOptions := []string{"none", "lax", "strict"}
|
||||
|
||||
for _, opt := range validOptions {
|
||||
validator.Clear()
|
||||
|
||||
config = newDefaultSessionConfig()
|
||||
config.SameSite = opt
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
}
|
||||
}
|
||||
|
@ -487,7 +782,7 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing
|
|||
config := newDefaultSessionConfig()
|
||||
config.Inactivity = -1
|
||||
config.Expiration = -1
|
||||
config.RememberMeDuration = schema.RememberMeDisabled
|
||||
config.RememberMe = schema.RememberMeDisabled
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
|
@ -496,7 +791,8 @@ func TestShouldSetDefaultWhenNegativeAndNotOverrideDisabledRememberMe(t *testing
|
|||
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
|
||||
assert.Equal(t, schema.RememberMeDisabled, config.RememberMeDuration)
|
||||
assert.Equal(t, schema.RememberMeDisabled, config.RememberMe)
|
||||
assert.True(t, config.DisableRememberMe)
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
|
||||
|
@ -505,7 +801,41 @@ func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
|
|||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.False(t, validator.HasErrors())
|
||||
assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration)
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
|
||||
assert.Equal(t, config.RememberMe, schema.DefaultSessionConfiguration.RememberMe)
|
||||
}
|
||||
|
||||
func TestShouldNotAllowLegacyAndModernCookiesConfig(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
config.Cookies = append(config.Cookies, schema.SessionCookieConfiguration{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: config.Name,
|
||||
Domain: config.Domain,
|
||||
SameSite: config.SameSite,
|
||||
Expiration: config.Expiration,
|
||||
Inactivity: config.Inactivity,
|
||||
RememberMe: config.RememberMe,
|
||||
},
|
||||
})
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
require.Len(t, validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(t, validator.Errors()[0], "session: option 'domain' and option 'cookies' can't be specified at the same time")
|
||||
}
|
||||
|
||||
func MustParseURL(uri string) *url.URL {
|
||||
u, err := url.ParseRequestURI(uri)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
|
|
@ -112,6 +112,7 @@ const (
|
|||
testInactivity = time.Second * 10
|
||||
testRedirectionURL = "http://redirection.local"
|
||||
testUsername = "john"
|
||||
exampleDotCom = "example.com"
|
||||
)
|
||||
|
||||
// Duo constants.
|
||||
|
|
|
@ -2,9 +2,9 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// CheckSafeRedirectionPOST handler checking whether the redirection to a given URL provided in body is safe.
|
||||
|
@ -16,24 +16,23 @@ func CheckSafeRedirectionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
var reqBody checkURIWithinDomainRequestBody
|
||||
var (
|
||||
bodyJSON checkURIWithinDomainRequestBody
|
||||
targetURI *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
err := ctx.ParseBody(&reqBody)
|
||||
if err != nil {
|
||||
if err = ctx.ParseBody(&bodyJSON); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to parse request body: %w", err), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
safe, err := utils.IsURIStringSafeRedirection(reqBody.URI, ctx.Configuration.Session.Domain)
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to determine if uri %s is safe to redirect to: %w", reqBody.URI, err), messageOperationFailed)
|
||||
if targetURI, err = url.ParseRequestURI(bodyJSON.URI); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to determine if uri %s is safe to redirect to: failed to parse URI '%s': %w", bodyJSON.URI, bodyJSON.URI, err), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{
|
||||
OK: safe,
|
||||
})
|
||||
if err != nil {
|
||||
if err = ctx.SetJSONBody(checkURIWithinDomainResponseBody{OK: ctx.IsSafeRedirectionTargetURI(targetURI)}); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to create response body: %w", err), messageOperationFailed)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,65 +4,79 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/mocks"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
var exampleDotComDomain = "example.com"
|
||||
|
||||
func TestCheckSafeRedirection_ForbiddenCall(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||
Username: "john",
|
||||
AuthenticationLevel: authentication.NotAuthenticated,
|
||||
})
|
||||
defer mock.Close()
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||
|
||||
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||
URI: "http://myapp.example.com",
|
||||
})
|
||||
|
||||
CheckSafeRedirectionPOST(mock.Ctx)
|
||||
assert.Equal(t, 401, mock.Ctx.Response.StatusCode())
|
||||
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_UnsafeRedirection(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||
Username: "john",
|
||||
AuthenticationLevel: authentication.OneFactor,
|
||||
})
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtxWithUserSession(t, tc.userSession)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||
|
||||
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||
URI: "http://myapp.com",
|
||||
URI: tc.have,
|
||||
})
|
||||
|
||||
CheckSafeRedirectionPOST(mock.Ctx)
|
||||
|
||||
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
|
||||
|
||||
if tc.expected == fasthttp.StatusOK {
|
||||
mock.Assert200OK(t, checkURIWithinDomainResponseBody{
|
||||
OK: false,
|
||||
OK: tc.ok,
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
|
||||
|
@ -70,7 +84,7 @@ func TestShouldFailOnInvalidBody(t *testing.T) {
|
|||
AuthenticationLevel: authentication.OneFactor,
|
||||
})
|
||||
defer mock.Close()
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotCom
|
||||
|
||||
mock.SetRequestBody(t, "not a valid json")
|
||||
|
||||
|
@ -84,7 +98,7 @@ func TestShouldFailOnInvalidURL(t *testing.T) {
|
|||
AuthenticationLevel: authentication.OneFactor,
|
||||
})
|
||||
defer mock.Close()
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
|
||||
mock.Ctx.Configuration.Session.Domain = exampleDotCom
|
||||
|
||||
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
|
||||
URI: "https//invalid-url",
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
|
@ -72,7 +71,25 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
|||
return
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
// TODO: write tests.
|
||||
provider, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("%s", err)
|
||||
|
||||
respondUnauthorized(ctx, messageAuthenticationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userSession, err := provider.GetSession(ctx.RequestCtx)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("%s", err)
|
||||
|
||||
respondUnauthorized(ctx, messageAuthenticationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newSession := session.NewDefaultUserSession()
|
||||
|
||||
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
|
||||
|
@ -84,7 +101,7 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
|||
return
|
||||
}
|
||||
|
||||
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.RegenerateSession(); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthType1FA, bodyJSON.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageAuthenticationFailed)
|
||||
|
@ -93,11 +110,11 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
|||
}
|
||||
|
||||
// Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data.
|
||||
keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != schema.RememberMeDisabled && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn
|
||||
keepMeLoggedIn := !provider.Config.DisableRememberMe && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn
|
||||
|
||||
// Set the cookie to expire if remember me is enabled and the user has asked us to.
|
||||
if keepMeLoggedIn {
|
||||
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe)
|
||||
err = provider.UpdateExpiration(ctx.RequestCtx, provider.Config.RememberMe)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionSave, "updated expiration", regulation.AuthType1FA, bodyJSON.Username, err)
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
type logoutBody struct {
|
||||
|
@ -26,14 +25,14 @@ func LogoutPOST(ctx *middlewares.AutheliaCtx) {
|
|||
ctx.Error(fmt.Errorf("unable to parse body during logout: %s", err), messageOperationFailed)
|
||||
}
|
||||
|
||||
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
|
||||
err = ctx.DestroySession()
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to destroy session during logout: %s", err), messageOperationFailed)
|
||||
}
|
||||
|
||||
redirectionURL, err := url.ParseRequestURI(body.TargetURL)
|
||||
if err == nil {
|
||||
responseBody.SafeTargetURL = utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain)
|
||||
responseBody.SafeTargetURL = ctx.IsSafeRedirectionTargetURI(redirectionURL)
|
||||
}
|
||||
|
||||
if body.TargetURL != "" {
|
||||
|
|
|
@ -246,7 +246,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
|
|||
func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *bodySignDuoRequest) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
|
||||
err := ctx.RegenerateSession()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username, err)
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/duo"
|
||||
"github.com/authelia/authelia/v4/internal/mocks"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
|
@ -579,6 +580,18 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotReturnRedirectURL() {
|
|||
func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
|
||||
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
|
||||
|
||||
s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "mydomain.local",
|
||||
},
|
||||
},
|
||||
}
|
||||
s.mock.StorageMock.EXPECT().
|
||||
LoadPreferredDuoDevice(s.mock.Ctx, "john").
|
||||
Return(&model.DuoDevice{ID: 1, Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"}, nil)
|
||||
|
|
|
@ -50,7 +50,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.RegenerateSession(); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeTOTP, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/mocks"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
|
@ -143,6 +144,18 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
|
|||
|
||||
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
||||
config := model.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
|
||||
s.mock.Ctx.Configuration.Session.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Domain: "mydomain.local",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.mock.StorageMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
|
|
|
@ -171,7 +171,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.RegenerateSession(); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
|
|
@ -259,9 +259,11 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
}
|
||||
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
sessionConfig := mock.Ctx.Configuration.Session
|
||||
|
||||
if resp.config != nil {
|
||||
mock.Ctx.Configuration = *resp.config
|
||||
mock.Ctx.Configuration.Session = sessionConfig
|
||||
}
|
||||
|
||||
// Set the initial user session.
|
||||
|
|
|
@ -19,14 +19,6 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
func isURLUnderProtectedDomain(url *url.URL, domain string) bool {
|
||||
return strings.HasSuffix(url.Hostname(), domain)
|
||||
}
|
||||
|
||||
func isSchemeHTTPS(url *url.URL) bool {
|
||||
return url.Scheme == "https"
|
||||
}
|
||||
|
||||
func isSchemeWSS(url *url.URL) bool {
|
||||
return url.Scheme == "wss"
|
||||
}
|
||||
|
@ -54,7 +46,7 @@ func parseBasicAuth(header []byte, auth string) (username, password string, err
|
|||
}
|
||||
|
||||
// isTargetURLAuthorized check whether the given user is authorized to access the resource.
|
||||
func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.URL,
|
||||
func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL *url.URL,
|
||||
username string, userGroups []string, clientIP net.IP, method []byte, authLevel authentication.Level) authorizationMatching {
|
||||
hasSubject, level := authorizer.GetRequiredLevel(
|
||||
authorization.Subject{
|
||||
|
@ -62,7 +54,7 @@ func isTargetURLAuthorized(authorizer *authorization.Authorizer, targetURL url.U
|
|||
Groups: userGroups,
|
||||
IP: clientIP,
|
||||
},
|
||||
authorization.NewObjectRaw(&targetURL, method))
|
||||
authorization.NewObjectRaw(targetURL, method))
|
||||
|
||||
switch {
|
||||
case level == authorization.Bypass:
|
||||
|
@ -128,13 +120,18 @@ func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string
|
|||
}
|
||||
|
||||
func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isUserAnonymous bool) (isInactiveTooLong bool) {
|
||||
if userSession.KeepMeLoggedIn || isUserAnonymous || int64(ctx.Providers.SessionProvider.Inactivity.Seconds()) == 0 {
|
||||
domainSession, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(ctx.Providers.SessionProvider.Inactivity).Before(ctx.Clock.Now())
|
||||
if userSession.KeepMeLoggedIn || isUserAnonymous || int64(domainSession.Config.Inactivity.Seconds()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(ctx.Providers.SessionProvider.Inactivity.Seconds()))
|
||||
isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(domainSession.Config.Inactivity).Before(ctx.Clock.Now())
|
||||
|
||||
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(domainSession.Config.Inactivity.Seconds()))
|
||||
|
||||
return isInactiveTooLong
|
||||
}
|
||||
|
@ -151,7 +148,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
|
|||
|
||||
if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) {
|
||||
// Destroy the session a new one will be regenerated on next request.
|
||||
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy session for user '%s' after the session has been inactive too long: %w", userSession.Username, err)
|
||||
}
|
||||
|
||||
|
@ -162,7 +159,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
|
|||
|
||||
if err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil {
|
||||
if err == authentication.ErrUserNotFound {
|
||||
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %v", err)
|
||||
}
|
||||
|
||||
|
@ -177,7 +174,7 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
|
|||
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, userSession.AuthenticationLevel, nil
|
||||
}
|
||||
|
||||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) {
|
||||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, cookieDomain string, isBasicAuth bool, username string, method []byte) {
|
||||
var (
|
||||
statusCode int
|
||||
friendlyUsername string
|
||||
|
@ -211,8 +208,8 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
|
|||
redirectionURL := ctxGetPortalURL(ctx)
|
||||
|
||||
if redirectionURL != nil {
|
||||
if !utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) {
|
||||
ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, ctx.Configuration.Session.Domain)
|
||||
if !utils.IsURISafeRedirection(redirectionURL, cookieDomain) {
|
||||
ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, cookieDomain)
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
|
@ -414,6 +411,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
|
|||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil {
|
||||
return isBasicAuth, username, name, groups, emails, authLevel, err
|
||||
}
|
||||
|
@ -422,7 +420,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
|
|||
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
|
||||
ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response")
|
||||
|
||||
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
|
||||
if err = ctx.DestroySession(); err != nil {
|
||||
ctx.Logger.Errorf("Unable to destroy user session after handler could not match them to their %s header: %s", headerSessionUsername, err)
|
||||
}
|
||||
|
||||
|
@ -447,7 +445,7 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
|
|||
return
|
||||
}
|
||||
|
||||
if !isSchemeHTTPS(targetURL) && !isSchemeWSS(targetURL) {
|
||||
if !utils.IsURISecure(targetURL) {
|
||||
ctx.Logger.Errorf("Scheme of target URL %s must be secure since cookies are "+
|
||||
"only transported over a secure connection for security reasons", targetURL.String())
|
||||
ctx.ReplyUnauthorized()
|
||||
|
@ -455,14 +453,32 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
|
|||
return
|
||||
}
|
||||
|
||||
if !isURLUnderProtectedDomain(targetURL, ctx.Configuration.Session.Domain) {
|
||||
ctx.Logger.Errorf("Target URL %s is not under the protected domain %s",
|
||||
targetURL.String(), ctx.Configuration.Session.Domain)
|
||||
cookieDomain := ctx.GetTargetURICookieDomain(targetURL)
|
||||
|
||||
if cookieDomain == "" {
|
||||
l := len(ctx.Configuration.Session.Cookies)
|
||||
|
||||
if l == 1 {
|
||||
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to the '%s' session cookie domain",
|
||||
targetURL.String(), ctx.Configuration.Session.Cookies[0].Domain)
|
||||
} else {
|
||||
domains := make([]string, 0, len(ctx.Configuration.Session.Cookies))
|
||||
|
||||
for i, domain := range ctx.Configuration.Session.Cookies {
|
||||
domains[i] = domain.Domain
|
||||
}
|
||||
|
||||
ctx.Logger.Errorf("Target URL '%s' was not detected as a match to any of the '%s' session cookie domains",
|
||||
targetURL.String(), strings.Join(domains, "', '"))
|
||||
}
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Target URL '%s' was detected as a match to the '%s' session cookie domain", targetURL.String(), cookieDomain)
|
||||
|
||||
method := ctx.XForwardedMethod()
|
||||
isBasicAuth, username, name, groups, emails, authLevel, err := verifyAuth(ctx, targetURL, refreshProfile, refreshProfileInterval)
|
||||
|
||||
|
@ -474,12 +490,12 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
|
|||
return
|
||||
}
|
||||
|
||||
handleUnauthorized(ctx, targetURL, isBasicAuth, username, method)
|
||||
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, *targetURL, username,
|
||||
authorized := isTargetURLAuthorized(ctx.Providers.Authorizer, targetURL, username,
|
||||
groups, ctx.RemoteIP(), method, authLevel)
|
||||
|
||||
switch authorized {
|
||||
|
@ -487,7 +503,7 @@ func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
|
|||
ctx.Logger.Infof("Access to %s is forbidden to user %s", targetURL.String(), username)
|
||||
ctx.ReplyForbidden()
|
||||
case NotAuthorized:
|
||||
handleUnauthorized(ctx, targetURL, isBasicAuth, username, method)
|
||||
handleUnauthorized(ctx, targetURL, cookieDomain, isBasicAuth, username, method)
|
||||
case Authorized:
|
||||
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,8 @@ func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {
|
|||
|
||||
func TestShouldRaiseWhenNoHeaderProvidedToDetectTargetURL(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
|
||||
|
||||
defer mock.Close()
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
|
@ -53,6 +55,7 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
_, err := mock.Ctx.GetOriginalURL()
|
||||
assert.Error(t, err)
|
||||
|
@ -162,7 +165,7 @@ func TestShouldCheckAuthorizationMatching(t *testing.T) {
|
|||
username = testUsername
|
||||
}
|
||||
|
||||
matching := isTargetURLAuthorized(authorizer, *u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel)
|
||||
matching := isTargetURLAuthorized(authorizer, u, username, []string{}, net.ParseIP("127.0.0.1"), []byte("GET"), rule.AuthLevel)
|
||||
assert.Equal(t, rule.ExpectedMatching, matching, "policy=%s, authLevel=%v, expected=%v, actual=%v",
|
||||
rule.Policy, rule.AuthLevel, rule.ExpectedMatching, matching)
|
||||
}
|
||||
|
@ -510,7 +513,6 @@ func TestShouldNotCrashOnEmptyEmail(t *testing.T) {
|
|||
userSession.AuthenticationLevel = authentication.OneFactor
|
||||
userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
|
||||
|
||||
fmt.Printf("Time is %v\n", userSession.RefreshTTL)
|
||||
err := mock.Ctx.SaveSession(userSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -663,32 +665,33 @@ func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
|
|||
{"https://deny.example.com", "john", []string{"john.doe@example.com"}, authentication.TwoFactor, 403},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.String(), func(t *testing.T) {
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.String(), func(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", tc.URL)
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testCase.Username
|
||||
userSession.Emails = testCase.Emails
|
||||
userSession.AuthenticationLevel = testCase.AuthenticationLevel
|
||||
userSession.Username = tc.Username
|
||||
userSession.Emails = tc.Emails
|
||||
userSession.AuthenticationLevel = tc.AuthenticationLevel
|
||||
userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
|
||||
|
||||
err := mock.Ctx.SaveSession(userSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL)
|
||||
|
||||
VerifyGET(verifyGetCfg)(mock.Ctx)
|
||||
expStatus, actualStatus := testCase.ExpectedStatusCode, mock.Ctx.Response.StatusCode()
|
||||
expStatus, actualStatus := tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode()
|
||||
assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d",
|
||||
testCase.URL, testCase.AuthenticationLevel, actualStatus, expStatus)
|
||||
tc.URL, tc.AuthenticationLevel, actualStatus, expStatus)
|
||||
|
||||
if testCase.ExpectedStatusCode == 200 && testCase.Username != "" {
|
||||
assert.Equal(t, []byte(testCase.Username), mock.Ctx.Response.Header.Peek("Remote-User"))
|
||||
fmt.Println(i)
|
||||
if tc.ExpectedStatusCode == 200 && tc.Username != "" {
|
||||
assert.Equal(t, tc.ExpectedStatusCode, mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(t, []byte(tc.Username), mock.Ctx.Response.Header.Peek("Remote-User"))
|
||||
assert.Equal(t, []byte("john.doe@example.com"), mock.Ctx.Response.Header.Peek("Remote-Email"))
|
||||
} else {
|
||||
assert.Equal(t, []byte(nil), mock.Ctx.Response.Header.Peek("Remote-User"))
|
||||
|
@ -706,10 +709,12 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
|||
clock.Set(time.Now())
|
||||
past := clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
// Reload the session provider since the configuration is indirect.
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testUsername
|
||||
|
@ -719,8 +724,6 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
|||
err := mock.Ctx.SaveSession(userSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||
|
||||
VerifyGET(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
// The session has been destroyed.
|
||||
|
@ -739,10 +742,10 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
|
|||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = time.Second * 10
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = time.Second * 10
|
||||
// Reload the session provider since the configuration is indirect.
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testUsername
|
||||
|
@ -768,7 +771,7 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te
|
|||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testUsername
|
||||
|
@ -800,7 +803,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T)
|
|||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
|
||||
past := mock.Clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
|
@ -836,10 +839,10 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
|
|||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
// Reload the session provider since the configuration is indirect.
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
|
||||
|
||||
past := clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
|
@ -899,7 +902,7 @@ func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *tes
|
|||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
|
||||
past := mock.Clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
|
@ -974,47 +977,6 @@ func TestShouldURLEncodeRedirectionHeader(t *testing.T) {
|
|||
string(mock.Ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestIsDomainProtected(t *testing.T) {
|
||||
GetURL := func(u string) *url.URL {
|
||||
x, err := url.ParseRequestURI(u)
|
||||
require.NoError(t, err)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
assert.True(t, isURLUnderProtectedDomain(
|
||||
GetURL("http://mytest.example.com/abc/?query=abc"), "example.com"))
|
||||
|
||||
assert.True(t, isURLUnderProtectedDomain(
|
||||
GetURL("http://example.com/abc/?query=abc"), "example.com"))
|
||||
|
||||
assert.True(t, isURLUnderProtectedDomain(
|
||||
GetURL("https://mytest.example.com/abc/?query=abc"), "example.com"))
|
||||
|
||||
// Cookies readable by a service on a machine is also readable by a service on the same machine
|
||||
// with a different port as mentioned in https://tools.ietf.org/html/rfc6265#section-8.5.
|
||||
assert.True(t, isURLUnderProtectedDomain(
|
||||
GetURL("https://mytest.example.com:8080/abc/?query=abc"), "example.com"))
|
||||
}
|
||||
|
||||
func TestSchemeIsHTTPS(t *testing.T) {
|
||||
GetURL := func(u string) *url.URL {
|
||||
x, err := url.ParseRequestURI(u)
|
||||
require.NoError(t, err)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
assert.False(t, isSchemeHTTPS(
|
||||
GetURL("http://mytest.example.com/abc/?query=abc")))
|
||||
assert.False(t, isSchemeHTTPS(
|
||||
GetURL("ws://mytest.example.com/abc/?query=abc")))
|
||||
assert.False(t, isSchemeHTTPS(
|
||||
GetURL("wss://mytest.example.com/abc/?query=abc")))
|
||||
assert.True(t, isSchemeHTTPS(
|
||||
GetURL("https://mytest.example.com/abc/?query=abc")))
|
||||
}
|
||||
|
||||
func TestSchemeIsWSS(t *testing.T) {
|
||||
GetURL := func(u string) *url.URL {
|
||||
x, err := url.ParseRequestURI(u)
|
||||
|
@ -1435,10 +1397,10 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.
|
|||
clock.Set(time.Now())
|
||||
past := clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
mock.Ctx.Configuration.Session.Cookies[0].Inactivity = testInactivity
|
||||
// Reload the session provider since the configuration is indirect.
|
||||
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
|
||||
assert.Equal(t, time.Second*10, mock.Ctx.Configuration.Session.Cookies[0].Inactivity)
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testUsername
|
||||
|
@ -1527,7 +1489,7 @@ func TestIsSessionInactiveTooLong(t *testing.T) {
|
|||
|
||||
defer ctx.Close()
|
||||
|
||||
ctx.Ctx.Configuration.Session.Inactivity = tc.inactivity
|
||||
ctx.Ctx.Configuration.Session.Cookies[0].Inactivity = tc.inactivity
|
||||
ctx.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil)
|
||||
|
||||
ctx.Clock.Set(tc.now)
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// Handle1FAResponse handle the redirection upon 1FA authentication.
|
||||
|
@ -57,7 +56,7 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
|
|||
return
|
||||
}
|
||||
|
||||
if !utils.IsURISafeRedirection(targetURL, ctx.Configuration.Session.Domain) {
|
||||
if !ctx.IsSafeRedirectionTargetURI(targetURL) {
|
||||
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
|
||||
|
||||
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
|
||||
|
@ -98,14 +97,18 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
|||
return
|
||||
}
|
||||
|
||||
var safe bool
|
||||
|
||||
if safe, err = utils.IsURIStringSafeRedirection(targetURI, ctx.Configuration.Session.Domain); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed)
|
||||
var (
|
||||
parsedURI *url.URL
|
||||
safe bool
|
||||
)
|
||||
|
||||
if parsedURI, err = url.ParseRequestURI(targetURI); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to determine if URI '%s' is safe to redirect to: failed to parse URI '%s': %w", targetURI, targetURI, err), messageMFAValidationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
safe = ctx.IsSafeRedirectionTargetURI(parsedURI)
|
||||
|
||||
if safe {
|
||||
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
||||
|
||||
|
|
|
@ -146,6 +146,7 @@ func TestWebauthnGetUserWithErr(t *testing.T) {
|
|||
|
||||
func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) {
|
||||
ctx := mocks.NewMockAutheliaCtx(t)
|
||||
ctx.Ctx.Request.Header.Del("X-Forwarded-Host")
|
||||
|
||||
w, err := newWebauthn(ctx.Ctx)
|
||||
|
||||
|
|
|
@ -227,9 +227,63 @@ func (ctx *AutheliaCtx) RootURLSlash() (issuerURL *url.URL) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetTargetURICookieDomain returns the session provider for the targetURI domain.
|
||||
func (ctx *AutheliaCtx) GetTargetURICookieDomain(targetURI *url.URL) string {
|
||||
hostname := targetURI.Hostname()
|
||||
|
||||
for _, domain := range ctx.Configuration.Session.Cookies {
|
||||
if utils.HasDomainSuffix(hostname, domain.Domain) {
|
||||
return domain.Domain
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsSafeRedirectionTargetURI returns true if the targetURI is within the scope of a cookie domain and secure.
|
||||
func (ctx *AutheliaCtx) IsSafeRedirectionTargetURI(targetURI *url.URL) bool {
|
||||
if !utils.IsURISecure(targetURI) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ctx.GetTargetURICookieDomain(targetURI) != ""
|
||||
}
|
||||
|
||||
// GetCookieDomain returns the cookie domain for the current request.
|
||||
func (ctx *AutheliaCtx) GetCookieDomain() (domain string, err error) {
|
||||
var targetURI *url.URL
|
||||
|
||||
if targetURI, err = ctx.GetOriginalURL(); err != nil {
|
||||
return "", fmt.Errorf("unable to retrieve cookie domain: %s", err)
|
||||
}
|
||||
|
||||
return ctx.GetTargetURICookieDomain(targetURI), nil
|
||||
}
|
||||
|
||||
// GetSessionProvider returns the session provider for the Request's domain.
|
||||
func (ctx *AutheliaCtx) GetSessionProvider() (provider *session.Session, err error) {
|
||||
var cookieDomain string
|
||||
|
||||
if cookieDomain, err = ctx.GetCookieDomain(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cookieDomain == "" {
|
||||
return nil, fmt.Errorf("unable to retrieve domain session: %s", err)
|
||||
}
|
||||
|
||||
return ctx.Providers.SessionProvider.Get(cookieDomain)
|
||||
}
|
||||
|
||||
// GetSession return the user session. Any update will be saved in cache.
|
||||
func (ctx *AutheliaCtx) GetSession() session.UserSession {
|
||||
userSession, err := ctx.Providers.SessionProvider.GetSession(ctx.RequestCtx)
|
||||
provider, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
ctx.Logger.Error("Unable to retrieve domain session")
|
||||
return session.NewDefaultUserSession()
|
||||
}
|
||||
|
||||
userSession, err := provider.GetSession(ctx.RequestCtx)
|
||||
if err != nil {
|
||||
ctx.Logger.Error("Unable to retrieve user session")
|
||||
return session.NewDefaultUserSession()
|
||||
|
@ -240,7 +294,32 @@ func (ctx *AutheliaCtx) GetSession() session.UserSession {
|
|||
|
||||
// SaveSession save the content of the session.
|
||||
func (ctx *AutheliaCtx) SaveSession(userSession session.UserSession) error {
|
||||
return ctx.Providers.SessionProvider.SaveSession(ctx.RequestCtx, userSession)
|
||||
provider, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save user session: %s", err)
|
||||
}
|
||||
|
||||
return provider.SaveSession(ctx.RequestCtx, userSession)
|
||||
}
|
||||
|
||||
// RegenerateSession regenerates user session.
|
||||
func (ctx *AutheliaCtx) RegenerateSession() error {
|
||||
provider, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to regenerate user session: %s", err)
|
||||
}
|
||||
|
||||
return provider.RegenerateSession(ctx.RequestCtx)
|
||||
}
|
||||
|
||||
// DestroySession destroy user session.
|
||||
func (ctx *AutheliaCtx) DestroySession() error {
|
||||
provider, err := ctx.GetSessionProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to destroy user session: %s", err)
|
||||
}
|
||||
|
||||
return provider.DestroySession(ctx.RequestCtx)
|
||||
}
|
||||
|
||||
// ReplyOK is a helper method to reply ok.
|
||||
|
|
|
@ -182,6 +182,8 @@ func TestShouldGetOriginalURLFromForwardedHeadersWithURI(t *testing.T) {
|
|||
|
||||
func TestShouldFallbackToNonXForwardedHeaders(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
|
||||
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password")
|
||||
|
@ -196,6 +198,8 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Del("X-Forwarded-Host")
|
||||
|
||||
mock.Ctx.RequestCtx.Request.SetRequestURI("/2fa/one-time-password")
|
||||
mock.Ctx.RequestCtx.Request.SetHost("localhost")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234")
|
||||
|
|
|
@ -51,9 +51,25 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
|||
mockAuthelia.Clock.Set(datetime)
|
||||
|
||||
config := schema.Configuration{}
|
||||
config.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
|
||||
config.Session.Name = "authelia_session"
|
||||
config.Session.Domain = "example.com"
|
||||
config.Session.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session",
|
||||
Domain: "example.com",
|
||||
RememberMe: schema.DefaultSessionConfiguration.RememberMe,
|
||||
Expiration: schema.DefaultSessionConfiguration.Expiration,
|
||||
},
|
||||
},
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: "authelia_session",
|
||||
Domain: "example2.com",
|
||||
RememberMe: schema.DefaultSessionConfiguration.RememberMe,
|
||||
Expiration: schema.DefaultSessionConfiguration.Expiration,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config.AccessControl.DefaultPolicy = "deny"
|
||||
config.AccessControl.Rules = []schema.ACLRule{{
|
||||
Domains: []string{"bypass.example.com"},
|
||||
|
@ -114,6 +130,9 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
|||
// Set a cookie to identify this client throughout the test.
|
||||
// request.Request.Header.SetCookie("authelia_session", "client_cookie").
|
||||
|
||||
// Set X-Forwarded-Host for compatibility with multi-root-domain implementation.
|
||||
request.Request.Header.Set("X-Forwarded-Host", "example.com")
|
||||
|
||||
ctx := middlewares.NewAutheliaCtx(request, config, providers)
|
||||
mockAuthelia.Ctx = ctx
|
||||
|
||||
|
|
|
@ -139,8 +139,6 @@ func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject
|
|||
|
||||
var client *gomail.Client
|
||||
|
||||
n.log.Debugf("creating client with %d options: %+v", len(n.opts), n.opts)
|
||||
|
||||
if client, err = gomail.NewClient(n.config.Host, n.opts...); err != nil {
|
||||
return fmt.Errorf("notifier: smtp: failed to establish client: %w", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Base":"{{ .Base }}",
|
||||
"DuoSelfEnrollment":"{{ .DuoSelfEnrollment }}",
|
||||
"LogoOverride":"{{ .LogoOverride }}",
|
||||
"RememberMe":"{{ .RememberMe }}",
|
||||
"ResetPassword":"{{ .ResetPassword }}",
|
||||
"ResetPasswordCustomURL":"{{ .ResetPasswordCustomURL }}",
|
||||
"Theme":"{{ .Theme }}"
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/random"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
|
@ -57,7 +58,16 @@ func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middle
|
|||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce))
|
||||
}
|
||||
|
||||
if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil {
|
||||
var (
|
||||
rememberMe string
|
||||
provider *session.Session
|
||||
)
|
||||
|
||||
if provider, err = ctx.GetSessionProvider(); err == nil {
|
||||
rememberMe = strconv.FormatBool(!provider.Config.DisableRememberMe)
|
||||
}
|
||||
|
||||
if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride, rememberMe)); err != nil {
|
||||
ctx.RequestCtx.Error("an error occurred", 503)
|
||||
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
|
||||
|
||||
|
@ -190,7 +200,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
|
|||
opts = &TemplatedFileOptions{
|
||||
AssetPath: config.Server.AssetPath,
|
||||
DuoSelfEnrollment: strFalse,
|
||||
RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled),
|
||||
RememberMe: strconv.FormatBool(!config.Session.DisableRememberMe),
|
||||
ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable),
|
||||
ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(),
|
||||
Theme: config.Theme,
|
||||
|
@ -227,7 +237,11 @@ type TemplatedFileOptions struct {
|
|||
}
|
||||
|
||||
// CommonData returns a TemplatedFileCommonData with the dynamic options.
|
||||
func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride string) TemplatedFileCommonData {
|
||||
func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData {
|
||||
if rememberMe != "" {
|
||||
return options.commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe)
|
||||
}
|
||||
|
||||
return TemplatedFileCommonData{
|
||||
Base: base,
|
||||
BaseURL: baseURL,
|
||||
|
@ -242,6 +256,22 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri
|
|||
}
|
||||
}
|
||||
|
||||
// CommonDataWithRememberMe returns a TemplatedFileCommonData with the dynamic options.
|
||||
func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData {
|
||||
return TemplatedFileCommonData{
|
||||
Base: base,
|
||||
BaseURL: baseURL,
|
||||
CSPNonce: nonce,
|
||||
LogoOverride: logoOverride,
|
||||
DuoSelfEnrollment: options.DuoSelfEnrollment,
|
||||
RememberMe: rememberMe,
|
||||
ResetPassword: options.ResetPassword,
|
||||
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
||||
Session: options.Session,
|
||||
Theme: options.Theme,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options.
|
||||
func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData {
|
||||
return TemplatedFileOpenAPIData{
|
||||
|
|
|
@ -9,6 +9,12 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// Serializer is a function that can serialize session information.
|
||||
type Serializer interface {
|
||||
Encode(src session.Dict) (data []byte, err error)
|
||||
Decode(dst *session.Dict, src []byte) (err error)
|
||||
}
|
||||
|
||||
// EncryptingSerializer a serializer encrypting the data with AES-GCM with 256-bit keys.
|
||||
type EncryptingSerializer struct {
|
||||
key [32]byte
|
||||
|
@ -21,7 +27,7 @@ func NewEncryptingSerializer(secret string) *EncryptingSerializer {
|
|||
}
|
||||
|
||||
// Encode encode and encrypt session.
|
||||
func (e *EncryptingSerializer) Encode(src session.Dict) ([]byte, error) {
|
||||
func (e *EncryptingSerializer) Encode(src session.Dict) (data []byte, err error) {
|
||||
if len(src.KV) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -31,16 +37,15 @@ func (e *EncryptingSerializer) Encode(src session.Dict) ([]byte, error) {
|
|||
return nil, fmt.Errorf("unable to marshal session: %v", err)
|
||||
}
|
||||
|
||||
encryptedDst, err := utils.Encrypt(dst, &e.key)
|
||||
if err != nil {
|
||||
if data, err = utils.Encrypt(dst, &e.key); err != nil {
|
||||
return nil, fmt.Errorf("unable to encrypt session: %v", err)
|
||||
}
|
||||
|
||||
return encryptedDst, nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Decode decrypt and decode session.
|
||||
func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error {
|
||||
func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) (err error) {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -49,12 +54,13 @@ func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error {
|
|||
delete(dst.KV, k)
|
||||
}
|
||||
|
||||
decryptedSrc, err := utils.Decrypt(src, &e.key)
|
||||
if err != nil {
|
||||
var data []byte
|
||||
|
||||
if data, err = utils.Decrypt(src, &e.key); err != nil {
|
||||
return fmt.Errorf("unable to decrypt session: %s", err)
|
||||
}
|
||||
|
||||
_, err = dst.UnmarshalMsg(decryptedSrc)
|
||||
_, err = dst.UnmarshalMsg(data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -2,158 +2,61 @@ package session
|
|||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
fasthttpsession "github.com/fasthttp/session/v2"
|
||||
"github.com/fasthttp/session/v2/providers/memory"
|
||||
"github.com/fasthttp/session/v2/providers/redis"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/fasthttp/session/v2"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
)
|
||||
|
||||
// Provider a session provider.
|
||||
// Provider contains a list of domain sessions.
|
||||
type Provider struct {
|
||||
sessionHolder *fasthttpsession.Session
|
||||
RememberMe time.Duration
|
||||
Inactivity time.Duration
|
||||
sessions map[string]*Session
|
||||
}
|
||||
|
||||
// NewProvider instantiate a session provider given a configuration.
|
||||
func NewProvider(config schema.SessionConfiguration, certPool *x509.CertPool) *Provider {
|
||||
c := NewProviderConfig(config, certPool)
|
||||
log := logging.Logger()
|
||||
|
||||
provider := new(Provider)
|
||||
provider.sessionHolder = fasthttpsession.New(c.config)
|
||||
name, p, s, err := NewSessionProvider(config, certPool)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
logger := logging.Logger()
|
||||
|
||||
provider.Inactivity, provider.RememberMe = config.Inactivity, config.RememberMeDuration
|
||||
provider := &Provider{
|
||||
sessions: map[string]*Session{},
|
||||
}
|
||||
|
||||
var (
|
||||
providerImpl fasthttpsession.Provider
|
||||
err error
|
||||
holder *session.Session
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
for _, dconfig := range config.Cookies {
|
||||
if _, holder, err = NewProviderConfigAndSession(dconfig, name, s, p); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = provider.sessionHolder.SetProvider(providerImpl)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
provider.sessions[dconfig.Domain] = &Session{
|
||||
Config: dconfig,
|
||||
sessionHolder: holder,
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
// GetSession return the user session from a request.
|
||||
func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
|
||||
store, err := p.sessionHolder.Get(ctx)
|
||||
|
||||
if err != nil {
|
||||
return NewDefaultUserSession(), err
|
||||
// Get returns session information for specified domain.
|
||||
func (p *Provider) Get(domain string) (*Session, error) {
|
||||
if domain == "" {
|
||||
return nil, fmt.Errorf("can not get session from an undefined domain")
|
||||
}
|
||||
|
||||
userSessionJSON, ok := store.Get(userSessionStorerKey).([]byte)
|
||||
s, found := p.sessions[domain]
|
||||
|
||||
// If userSession is not yet defined we create the new session with default values
|
||||
// and save it in the store.
|
||||
if !ok {
|
||||
userSession := NewDefaultUserSession()
|
||||
|
||||
store.Set(userSessionStorerKey, userSession)
|
||||
|
||||
return userSession, nil
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no session found for domain '%s'", domain)
|
||||
}
|
||||
|
||||
var userSession UserSession
|
||||
err = json.Unmarshal(userSessionJSON, &userSession)
|
||||
|
||||
if err != nil {
|
||||
return NewDefaultUserSession(), err
|
||||
}
|
||||
|
||||
return userSession, nil
|
||||
}
|
||||
|
||||
// SaveSession save the user session.
|
||||
func (p *Provider) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) error {
|
||||
store, err := p.sessionHolder.Get(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userSessionJSON, err := json.Marshal(userSession)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Set(userSessionStorerKey, userSessionJSON)
|
||||
|
||||
err = p.sessionHolder.Save(ctx, store)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegenerateSession regenerate a session ID.
|
||||
func (p *Provider) RegenerateSession(ctx *fasthttp.RequestCtx) error {
|
||||
err := p.sessionHolder.Regenerate(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DestroySession destroy a session ID and delete the cookie.
|
||||
func (p *Provider) DestroySession(ctx *fasthttp.RequestCtx) error {
|
||||
return p.sessionHolder.Destroy(ctx)
|
||||
}
|
||||
|
||||
// UpdateExpiration update the expiration of the cookie and session.
|
||||
func (p *Provider) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) error {
|
||||
store, err := p.sessionHolder.Get(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.SetExpiration(expiration)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.sessionHolder.Save(ctx, store)
|
||||
}
|
||||
|
||||
// GetExpiration get the expiration of the current session.
|
||||
func (p *Provider) GetExpiration(ctx *fasthttp.RequestCtx) (time.Duration, error) {
|
||||
store, err := p.sessionHolder.Get(ctx)
|
||||
|
||||
if err != nil {
|
||||
return time.Duration(0), err
|
||||
}
|
||||
|
||||
return store.GetExpiration(), nil
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/fasthttp/session/v2"
|
||||
"github.com/fasthttp/session/v2/providers/memory"
|
||||
"github.com/fasthttp/session/v2/providers/redis"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
@ -18,7 +19,7 @@ import (
|
|||
)
|
||||
|
||||
// NewProviderConfig creates a configuration for creating the session provider.
|
||||
func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPool) ProviderConfig {
|
||||
func NewProviderConfig(config schema.SessionCookieConfiguration, providerName string, serializer Serializer) ProviderConfig {
|
||||
c := session.NewDefaultConfig()
|
||||
|
||||
c.SessionIDGeneratorFunc = func() []byte {
|
||||
|
@ -61,16 +62,42 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
|
|||
return true
|
||||
}
|
||||
|
||||
var redisConfig *redis.Config
|
||||
if serializer != nil {
|
||||
c.EncodeFunc = serializer.Encode
|
||||
c.DecodeFunc = serializer.Decode
|
||||
}
|
||||
|
||||
var redisSentinelConfig *redis.FailoverConfig
|
||||
return ProviderConfig{
|
||||
c,
|
||||
providerName,
|
||||
}
|
||||
}
|
||||
|
||||
var providerName string
|
||||
func NewProviderSession(pconfig ProviderConfig, provider session.Provider) (p *session.Session, err error) {
|
||||
p = session.New(pconfig.config)
|
||||
|
||||
if err = p.SetProvider(provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewProviderConfigAndSession(config schema.SessionCookieConfiguration, providerName string, serializer Serializer, provider session.Provider) (c ProviderConfig, p *session.Session, err error) {
|
||||
c = NewProviderConfig(config, providerName, serializer)
|
||||
|
||||
if p, err = NewProviderSession(c, provider); err != nil {
|
||||
return c, nil, err
|
||||
}
|
||||
|
||||
return c, p, nil
|
||||
}
|
||||
|
||||
func NewSessionProvider(config schema.SessionConfiguration, certPool *x509.CertPool) (name string, provider session.Provider, serializer Serializer, err error) {
|
||||
// If redis configuration is provided, then use the redis provider.
|
||||
switch {
|
||||
case config.Redis != nil:
|
||||
serializer := NewEncryptingSerializer(config.Secret)
|
||||
serializer = NewEncryptingSerializer(config.Secret)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
|
@ -92,8 +119,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
|
|||
}
|
||||
}
|
||||
|
||||
providerName = "redis-sentinel"
|
||||
redisSentinelConfig = &redis.FailoverConfig{
|
||||
name = "redis-sentinel"
|
||||
|
||||
provider, err = redis.NewFailoverCluster(redis.FailoverConfig{
|
||||
Logger: logging.LoggerCtxPrintf(logrus.TraceLevel),
|
||||
MasterName: config.Redis.HighAvailability.SentinelName,
|
||||
SentinelAddrs: addrs,
|
||||
|
@ -109,9 +137,9 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
|
|||
IdleTimeout: 300,
|
||||
TLSConfig: tlsConfig,
|
||||
KeyPrefix: "authelia-session",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
providerName = "redis"
|
||||
name = "redis"
|
||||
network := "tcp"
|
||||
|
||||
var addr string
|
||||
|
@ -123,7 +151,7 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
|
|||
addr = fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port)
|
||||
}
|
||||
|
||||
redisConfig = &redis.Config{
|
||||
provider, err = redis.New(redis.Config{
|
||||
Logger: logging.LoggerCtxPrintf(logrus.TraceLevel),
|
||||
Network: network,
|
||||
Addr: addr,
|
||||
|
@ -135,19 +163,12 @@ func NewProviderConfig(config schema.SessionConfiguration, certPool *x509.CertPo
|
|||
IdleTimeout: 300,
|
||||
TLSConfig: tlsConfig,
|
||||
KeyPrefix: "authelia-session",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.EncodeFunc = serializer.Encode
|
||||
c.DecodeFunc = serializer.Decode
|
||||
default:
|
||||
providerName = "memory"
|
||||
name = "memory"
|
||||
provider, err = memory.New(memory.Config{})
|
||||
}
|
||||
|
||||
return ProviderConfig{
|
||||
c,
|
||||
redisConfig,
|
||||
redisSentinelConfig,
|
||||
providerName,
|
||||
}
|
||||
return name, provider, serializer, err
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
}
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
|
@ -14,16 +13,31 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
)
|
||||
|
||||
func newTestSession() (*Session, error) {
|
||||
config := schema.SessionConfiguration{}
|
||||
config.Cookies = []schema.SessionCookieConfiguration{
|
||||
{
|
||||
SessionCookieCommonConfiguration: schema.SessionCookieCommonConfiguration{
|
||||
Name: testName,
|
||||
Domain: testDomain,
|
||||
Expiration: testExpiration,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider := NewProvider(config, nil)
|
||||
|
||||
return provider.Get(testDomain)
|
||||
}
|
||||
|
||||
func TestShouldInitializerSession(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
configuration := schema.SessionConfiguration{}
|
||||
configuration.Domain = testDomain
|
||||
configuration.Name = testName
|
||||
configuration.Expiration = testExpiration
|
||||
|
||||
provider := NewProvider(configuration, nil)
|
||||
provider, err := newTestSession()
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err := provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, NewDefaultUserSession(), session)
|
||||
}
|
||||
|
@ -31,22 +45,19 @@ func TestShouldInitializerSession(t *testing.T) {
|
|||
func TestShouldUpdateSession(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
|
||||
configuration := schema.SessionConfiguration{}
|
||||
configuration.Domain = testDomain
|
||||
configuration.Name = testName
|
||||
configuration.Expiration = testExpiration
|
||||
provider, err := newTestSession()
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider := NewProvider(configuration, nil)
|
||||
session, _ := provider.GetSession(ctx)
|
||||
|
||||
session.Username = testUsername
|
||||
session.AuthenticationLevel = authentication.TwoFactor
|
||||
|
||||
err := provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
err = provider.SaveSession(ctx, session)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, UserSession{
|
||||
Username: testUsername,
|
||||
|
@ -56,26 +67,23 @@ func TestShouldUpdateSession(t *testing.T) {
|
|||
|
||||
func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
configuration := schema.SessionConfiguration{}
|
||||
|
||||
timeOneFactor := time.Unix(1625048140, 0)
|
||||
timeTwoFactor := time.Unix(1625048150, 0)
|
||||
timeZeroFactor := time.Unix(0, 0)
|
||||
|
||||
configuration.Domain = testDomain
|
||||
configuration.Name = testName
|
||||
configuration.Expiration = testExpiration
|
||||
provider, err := newTestSession()
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider := NewProvider(configuration, nil)
|
||||
session, _ := provider.GetSession(ctx)
|
||||
|
||||
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
|
||||
|
||||
err := provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
err = provider.SaveSession(ctx, session)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
authAt, err := session.AuthenticatedTime(authorization.OneFactor)
|
||||
assert.NoError(t, err)
|
||||
|
@ -100,10 +108,10 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
|||
session.SetTwoFactorDuo(timeTwoFactor)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, UserSession{
|
||||
Username: testUsername,
|
||||
|
@ -129,26 +137,23 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
|||
|
||||
func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
configuration := schema.SessionConfiguration{}
|
||||
|
||||
timeOneFactor := time.Unix(1625048140, 0)
|
||||
timeTwoFactor := time.Unix(1625048150, 0)
|
||||
timeZeroFactor := time.Unix(0, 0)
|
||||
|
||||
configuration.Domain = testDomain
|
||||
configuration.Name = testName
|
||||
configuration.Expiration = testExpiration
|
||||
provider, err := newTestSession()
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider := NewProvider(configuration, nil)
|
||||
session, _ := provider.GetSession(ctx)
|
||||
|
||||
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
|
||||
|
||||
err := provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
err = provider.SaveSession(ctx, session)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
authAt, err := session.AuthenticatedTime(authorization.OneFactor)
|
||||
assert.NoError(t, err)
|
||||
|
@ -173,10 +178,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs)
|
||||
assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())
|
||||
|
@ -196,10 +201,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
|
||||
|
@ -208,10 +213,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
|
||||
|
@ -220,10 +225,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
|
||||
|
@ -232,10 +237,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
|
||||
|
@ -244,10 +249,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
|
||||
|
@ -256,10 +261,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
|
||||
|
@ -268,10 +273,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorTOTP(timeTwoFactor)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
|
||||
|
@ -280,10 +285,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
session.SetTwoFactorTOTP(timeTwoFactor)
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
|
||||
|
@ -292,31 +297,28 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
|||
|
||||
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
configuration := schema.SessionConfiguration{}
|
||||
configuration.Domain = testDomain
|
||||
configuration.Name = testName
|
||||
configuration.Expiration = testExpiration
|
||||
domainSession, err := newTestSession()
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider := NewProvider(configuration, nil)
|
||||
session, err := provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
session, err := domainSession.GetSession(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session.Username = testUsername
|
||||
session.AuthenticationLevel = authentication.TwoFactor
|
||||
|
||||
err = provider.SaveSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
err = domainSession.SaveSession(ctx, session)
|
||||
assert.NoError(t, err)
|
||||
|
||||
newUserSession, err := provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
newUserSession, err := domainSession.GetSession(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testUsername, newUserSession.Username)
|
||||
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
|
||||
|
||||
err = provider.DestroySession(ctx)
|
||||
require.NoError(t, err)
|
||||
err = domainSession.DestroySession(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
newUserSession, err = provider.GetSession(ctx)
|
||||
require.NoError(t, err)
|
||||
newUserSession, err = domainSession.GetSession(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", newUserSession.Username)
|
||||
assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,8 +3,7 @@ package session
|
|||
import (
|
||||
"time"
|
||||
|
||||
session "github.com/fasthttp/session/v2"
|
||||
"github.com/fasthttp/session/v2/providers/redis"
|
||||
"github.com/fasthttp/session/v2"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
|
@ -14,8 +13,6 @@ import (
|
|||
// ProviderConfig is the configuration used to create the session provider.
|
||||
type ProviderConfig struct {
|
||||
config session.Config
|
||||
redisConfig *redis.Config
|
||||
redisSentinelConfig *redis.FailoverConfig
|
||||
providerName string
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -23,7 +23,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -20,10 +20,13 @@ authentication_backend:
|
|||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
cookies:
|
||||
- name: 'authelia_session'
|
||||
domain: 'example.com'
|
||||
authelia_url: 'https://login.example.com'
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
|
|
|
@ -21,10 +21,12 @@ authentication_backend:
|
|||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
cookies:
|
||||
- name: 'authelia_session'
|
||||
domain: 'example.com'
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -23,7 +23,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -102,7 +102,7 @@ session:
|
|||
- host: redis-sentinel-2
|
||||
port: 26379
|
||||
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
|
|
|
@ -38,7 +38,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
|
|
|
@ -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
|
||||
...
|
|
@ -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'
|
||||
...
|
|
@ -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
|
||||
...
|
|
@ -25,7 +25,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
|
|
|
@ -23,7 +23,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
|
|
|
@ -16,10 +16,13 @@ authentication_backend:
|
|||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
|
||||
cookies:
|
||||
- domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# We use redis here to keep the users authenticated when Authelia restarts
|
||||
# It eases development.
|
||||
redis:
|
||||
|
|
|
@ -19,7 +19,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
# We use redis here to keep the users authenticated when Authelia restarts
|
||||
# It eases development.
|
||||
redis:
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
|
|
|
@ -21,10 +21,12 @@ authentication_backend:
|
|||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com
|
||||
inactivity: 5
|
||||
expiration: 8
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -25,9 +25,9 @@ authentication_backend:
|
|||
|
||||
session:
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
expiration: 3600
|
||||
inactivity: 300
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
@ -81,6 +81,7 @@ access_control:
|
|||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
|
||||
regulation:
|
||||
# Set it to 0 to disable max_retries.
|
||||
max_retries: 3
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
|
|
|
@ -24,7 +24,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
|
|
|
@ -45,14 +45,14 @@ click:
|
|||
}
|
||||
|
||||
// Login 1FA.
|
||||
func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, targetURL string) {
|
||||
rs.doVisitLoginPage(t, page, targetURL)
|
||||
func (rs *RodSession) doLoginOneFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, domain string, targetURL string) {
|
||||
rs.doVisitLoginPage(t, page, domain, targetURL)
|
||||
rs.doFillLoginPageAndClick(t, page, username, password, keepMeLoggedIn)
|
||||
}
|
||||
|
||||
// Login 1FA and 2FA subsequently (must already be registered).
|
||||
func (rs *RodSession) doLoginTwoFactor(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool, otpSecret, targetURL string) {
|
||||
rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, targetURL)
|
||||
rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, BaseDomain, targetURL)
|
||||
rs.verifyIsSecondFactorPage(t, page)
|
||||
rs.doValidateTOTP(t, page, otpSecret)
|
||||
// timeout when targetURL is not defined to prevent a show stopping redirect when visiting a protected domain.
|
||||
|
@ -63,9 +63,9 @@ func (rs *RodSession) doLoginTwoFactor(t *testing.T, page *rod.Page, username, p
|
|||
|
||||
// Login 1FA and register 2FA.
|
||||
func (rs *RodSession) doLoginAndRegisterTOTP(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool) string {
|
||||
rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, "")
|
||||
rs.doLoginOneFactor(t, page, username, password, keepMeLoggedIn, BaseDomain, "")
|
||||
secret := rs.doRegisterTOTP(t, page)
|
||||
rs.doVisit(t, page, GetLoginBaseURL())
|
||||
rs.doVisit(t, page, GetLoginBaseURL(BaseDomain))
|
||||
rs.verifyIsSecondFactorPage(t, page)
|
||||
|
||||
return secret
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) doLogout(t *testing.T, page *rod.Page) {
|
||||
rs.doVisit(t, page, fmt.Sprintf("%s%s", GetLoginBaseURL(), "/logout"))
|
||||
rs.doVisit(t, page, fmt.Sprintf("%s%s", GetLoginBaseURL(BaseDomain), "/logout"))
|
||||
rs.verifyIsFirstFactorPage(t, page)
|
||||
}
|
||||
|
||||
func (rs *RodSession) doLogoutWithRedirect(t *testing.T, page *rod.Page, targetURL string, firstFactor bool) {
|
||||
rs.doVisit(t, page, fmt.Sprintf("%s%s%s", GetLoginBaseURL(), "/logout?rd=", url.QueryEscape(targetURL)))
|
||||
rs.doVisit(t, page, fmt.Sprintf("%s%s%s", GetLoginBaseURL(BaseDomain), "/logout?rd=", url.QueryEscape(targetURL)))
|
||||
|
||||
if firstFactor {
|
||||
rs.verifyIsFirstFactorPage(t, page)
|
||||
|
|
|
@ -26,11 +26,11 @@ func (rs *RodSession) doVisitAndVerifyOneFactorStep(t *testing.T, page *rod.Page
|
|||
rs.verifyIsFirstFactorPage(t, page)
|
||||
}
|
||||
|
||||
func (rs *RodSession) doVisitLoginPage(t *testing.T, page *rod.Page, targetURL string) {
|
||||
func (rs *RodSession) doVisitLoginPage(t *testing.T, page *rod.Page, baseDomain string, targetURL string) {
|
||||
suffix := ""
|
||||
if targetURL != "" {
|
||||
suffix = fmt.Sprintf("?rd=%s", targetURL)
|
||||
}
|
||||
|
||||
rs.doVisitAndVerifyOneFactorStep(t, page, fmt.Sprintf("%s/%s", GetLoginBaseURL(), suffix))
|
||||
rs.doVisitAndVerifyOneFactorStep(t, page, fmt.Sprintf("%s/%s", GetLoginBaseURL(baseDomain), suffix))
|
||||
}
|
||||
|
|
|
@ -8,16 +8,38 @@ import (
|
|||
)
|
||||
|
||||
// BaseDomain the base domain.
|
||||
var BaseDomain = "example.com:8080"
|
||||
var (
|
||||
BaseDomain = "example.com:8080"
|
||||
Example2DotCom = "example2.com:8080"
|
||||
Example3DotCom = "example3.com:8080"
|
||||
)
|
||||
|
||||
// PathPrefix the prefix/url_base of the login portal.
|
||||
var PathPrefix = os.Getenv("PathPrefix")
|
||||
|
||||
// LoginBaseURLFmt the base URL of the login portal for specified baseDomain.
|
||||
func LoginBaseURLFmt(baseDomain string) string {
|
||||
if baseDomain == "" {
|
||||
baseDomain = BaseDomain
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://login.%s", baseDomain)
|
||||
}
|
||||
|
||||
// LoginBaseURL the base URL of the login portal.
|
||||
var LoginBaseURL = fmt.Sprintf("https://login.%s", BaseDomain)
|
||||
var LoginBaseURL = LoginBaseURLFmt(BaseDomain)
|
||||
|
||||
// SingleFactorBaseURLFmt the base URL of the singlefactor with custom domain.
|
||||
func SingleFactorBaseURLFmt(baseDomain string) string {
|
||||
if baseDomain == "" {
|
||||
baseDomain = BaseDomain
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://singlefactor.%s", baseDomain)
|
||||
}
|
||||
|
||||
// SingleFactorBaseURL the base URL of the singlefactor domain.
|
||||
var SingleFactorBaseURL = fmt.Sprintf("https://singlefactor.%s", BaseDomain)
|
||||
var SingleFactorBaseURL = SingleFactorBaseURLFmt(BaseDomain)
|
||||
|
||||
// AdminBaseURL the base URL of the admin domain.
|
||||
var AdminBaseURL = fmt.Sprintf("https://admin.%s", BaseDomain)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -22,7 +21,6 @@ func waitUntilServiceLogDetected(
|
|||
|
||||
err := utils.CheckUntil(5*time.Second, 1*time.Minute, func() (bool, error) {
|
||||
logs, err := dockerEnvironment.Logs(service, []string{"--tail", "20"})
|
||||
fmt.Printf(".")
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -35,8 +33,6 @@ func waitUntilServiceLogDetected(
|
|||
return false, nil
|
||||
})
|
||||
|
||||
fmt.Print("\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -55,7 +51,7 @@ func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) erro
|
|||
90*time.Second,
|
||||
dockerEnvironment,
|
||||
"authelia-frontend",
|
||||
[]string{"dev server running at", "ready in"})
|
||||
[]string{"dev server running at", "ready in", "server restarted"})
|
||||
}
|
||||
|
||||
func waitUntilK3DIsReady(dockerEnvironment *DockerEnvironment) error {
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
<!DOCTYPE>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Home page</title>
|
||||
<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>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Public resource</title>
|
||||
<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>
|
||||
<body>
|
||||
<h1>Public resource</h1>
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Public resource</title>
|
||||
<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>
|
||||
<body>
|
||||
<h1>Public resource</h1>
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -3,6 +3,22 @@
|
|||
<head>
|
||||
<title>Secret</title>
|
||||
<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>
|
||||
|
||||
<body id="secret">
|
||||
|
|
|
@ -9,43 +9,42 @@ http {
|
|||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/home;
|
||||
server_name home.example.com;
|
||||
server_name ~^home\.example([0-9])*\.com$;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/public;
|
||||
server_name public.example.com;
|
||||
server_name ~^public\.example([0-9])*\.com$;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/secure;
|
||||
server_name secure.example.com;
|
||||
server_name ~^secure\.example([0-9])*\.com$;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/admin;
|
||||
server_name admin.example.com;
|
||||
server_name ~^admin\.example([0-9])*\.com$;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/dev;
|
||||
server_name dev.example.com;
|
||||
server_name ~^dev\.example([0-9])*\.com$;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
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 {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/singlefactor;
|
||||
server_name singlefactor.example.com;
|
||||
server_name ~^singlefactor\.example([0-9])*\.com$;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# You can find a documented example of configuration in ./docs/proxies/nginx.md.
|
||||
#
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
@ -10,7 +9,7 @@ events {
|
|||
http {
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name login.example.com;
|
||||
server_name ~^login\.example([0-9])*\.com$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
set $frontend_endpoint http://authelia-frontend:3000;
|
||||
|
@ -114,12 +113,17 @@ http {
|
|||
|
||||
proxy_pass $frontend_endpoint;
|
||||
}
|
||||
|
||||
# Proxies requests to backend for dev workflow.
|
||||
location /devworkflow {
|
||||
proxy_pass $backend_endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
# Serves the home page.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name home.example.com;
|
||||
server_name ~^home\.example([0-9])*\.com$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://nginx-backend;
|
||||
|
@ -141,12 +145,7 @@ http {
|
|||
# Example configuration of domains protected by Authelia.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name public.example.com
|
||||
admin.example.com
|
||||
secure.example.com
|
||||
dev.example.com
|
||||
singlefactor.example.com
|
||||
mx1.mail.example.com mx2.mail.example.com;
|
||||
server_name ~^(public|admin|secure|dev|singlefactor|mx[1-2])(\.mail)?\.(?<basedomain>example([0-9])*\.com)$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
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
|
||||
# URL with the correct redirection parameter.
|
||||
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;
|
||||
}
|
||||
|
@ -245,7 +244,7 @@ http {
|
|||
proxy_set_header Remote-Email $email;
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -254,8 +253,7 @@ http {
|
|||
# Example configuration of domains protected by Authelia.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name oidc.example.com
|
||||
oidc-public.example.com;
|
||||
server_name ~^oidc(-public)?\.(?<basedomain>example([0-9])*\.com)$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
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
|
||||
# URL with the correct redirection parameter.
|
||||
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;
|
||||
}
|
||||
|
@ -322,7 +320,7 @@ http {
|
|||
# Fake Web Mail used to receive emails sent by Authelia.
|
||||
server {
|
||||
listen 8080 ssl;
|
||||
server_name mail.example.com;
|
||||
server_name ~^mail\.example([0-9])*\.com$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://smtp:1080;
|
||||
|
@ -344,7 +342,7 @@ http {
|
|||
# Fake API emulating Duo behavior
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name duo.example.com;
|
||||
server_name ~^duo\.example([0-9])*\.com$;
|
||||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://duo-api:3000;
|
||||
|
|
|
@ -86,7 +86,7 @@ access_control:
|
|||
session:
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
remember_me: 1y
|
||||
domain: example.com
|
||||
redis:
|
||||
host: redis-service
|
||||
|
|
|
@ -56,7 +56,7 @@ func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
|
|||
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")
|
||||
err := methodsButton.Click("left", 1)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue