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