fix(session): ensure default cookie samesite value is lax (#1926)
This implements a change to the default behaviour of the cookies generated by the sessions package. The old behaviour was to set the SameSite=None, this changes it to SameSite=Lax. Additionally this puts the option in the hands of the end-user so they can decide for themselves what the best option is.pull/1928/head
parent
2f1e45071a
commit
706fbfdb2c
|
@ -338,6 +338,15 @@ 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/docs/configuration/session.html#same_site
|
||||
same_site: lax
|
||||
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||
secret: insecure_session_secret
|
||||
|
@ -359,11 +368,6 @@ session:
|
|||
## Value of 0 disables remember me.
|
||||
remember_me_duration: 1M
|
||||
|
||||
## 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
|
||||
|
||||
##
|
||||
## Redis Provider
|
||||
##
|
||||
|
|
|
@ -22,6 +22,7 @@ and can then order the reverse proxy to let the request pass through to the appl
|
|||
session:
|
||||
name: authelia_session
|
||||
domain: example.com
|
||||
same_site: lax
|
||||
secret: unsecure_session_secret
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
|
@ -67,6 +68,26 @@ 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
|
||||
<div markdown="1">
|
||||
type: string
|
||||
{: .label .label-config .label-purple }
|
||||
default: lax
|
||||
{: .label .label-config .label-blue }
|
||||
required: no
|
||||
{: .label .label-config .label-green }
|
||||
</div>
|
||||
|
||||
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
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -9,11 +9,13 @@ nav_order: 1
|
|||
|
||||
## Protection against cookie theft
|
||||
|
||||
Authelia uses two mechanisms to protect against cookie theft:
|
||||
1. session attribute `httpOnly` set to true make client-side code unable to
|
||||
read the cookie.
|
||||
2. session attribute `secure` ensure the cookie will never be sent over an
|
||||
insecure HTTP connections.
|
||||
Authelia sets several key cookie attributes to prevent cookie theft:
|
||||
1. `HttpOnly` is set forbidding client-side code like javascript from access to the cookie.
|
||||
2. `Secure` is set forbidding the browser from sending the cookie to sites which do not use the https scheme.
|
||||
3. `SameSite` is by default set to `Lax` which prevents it being sent over cross-origin requests.
|
||||
|
||||
Read about these attributes in detail on the
|
||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
|
||||
|
||||
## Protection against multi-domain cookie attacks
|
||||
|
||||
|
|
|
@ -338,6 +338,15 @@ 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/docs/configuration/session.html#same_site
|
||||
same_site: lax
|
||||
|
||||
## The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
|
||||
## Secret can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||
secret: insecure_session_secret
|
||||
|
@ -359,11 +368,6 @@ session:
|
|||
## Value of 0 disables remember me.
|
||||
remember_me_duration: 1M
|
||||
|
||||
## 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
|
||||
|
||||
##
|
||||
## Redis Provider
|
||||
##
|
||||
|
|
|
@ -31,11 +31,12 @@ type RedisSessionConfiguration struct {
|
|||
// SessionConfiguration represents the configuration related to user sessions.
|
||||
type SessionConfiguration struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
SameSite string `mapstructure:"same_site"`
|
||||
Secret string `mapstructure:"secret"`
|
||||
Expiration string `mapstructure:"expiration"`
|
||||
Inactivity string `mapstructure:"inactivity"`
|
||||
RememberMeDuration string `mapstructure:"remember_me_duration"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
Redis *RedisSessionConfiguration `mapstructure:"redis"`
|
||||
}
|
||||
|
||||
|
@ -45,4 +46,5 @@ var DefaultSessionConfiguration = SessionConfiguration{
|
|||
Expiration: "1h",
|
||||
Inactivity: "5m",
|
||||
RememberMeDuration: "1M",
|
||||
SameSite: "lax",
|
||||
}
|
||||
|
|
|
@ -85,10 +85,11 @@ var validKeys = []string{
|
|||
|
||||
// Session Keys.
|
||||
"session.name",
|
||||
"session.domain",
|
||||
"session.same_site",
|
||||
"session.expiration",
|
||||
"session.inactivity",
|
||||
"session.remember_me_duration",
|
||||
"session.domain",
|
||||
|
||||
// Redis Session Keys.
|
||||
"session.redis.host",
|
||||
|
|
|
@ -56,6 +56,12 @@ func validateSession(configuration *schema.SessionConfiguration, validator *sche
|
|||
if strings.Contains(configuration.Domain, "*") {
|
||||
validator.Push(errors.New("The domain of the session must be the root domain you're protecting instead of a wildcard domain"))
|
||||
}
|
||||
|
||||
if configuration.SameSite == "" {
|
||||
configuration.SameSite = schema.DefaultSessionConfiguration.SameSite
|
||||
} else if configuration.SameSite != "none" && configuration.SameSite != "lax" && configuration.SameSite != "strict" {
|
||||
validator.Push(errors.New("session same_site is configured incorrectly, must be one of 'none', 'lax', or 'strict'"))
|
||||
}
|
||||
}
|
||||
|
||||
func validateRedis(configuration *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
|
|
|
@ -51,6 +51,17 @@ func TestShouldSetDefaultSessionExpiration(t *testing.T) {
|
|||
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultSessionSameSite(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.False(t, validator.HasErrors())
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite)
|
||||
}
|
||||
|
||||
func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
@ -381,6 +392,34 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
|
|||
assert.EqualError(t, validator.Errors()[0], "The domain of the session must be the root domain you're protecting instead of a wildcard domain")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.SameSite = "NOne"
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "session same_site is configured incorrectly, must be one of 'none', 'lax', or 'strict'")
|
||||
}
|
||||
|
||||
func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
validOptions := []string{"none", "lax", "strict"}
|
||||
|
||||
for _, opt := range validOptions {
|
||||
config.SameSite = opt
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenBadInactivityAndExpirationSet(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
|
|
@ -24,6 +24,18 @@ func NewProviderConfig(configuration schema.SessionConfiguration, certPool *x509
|
|||
// Set the cookie to the given domain.
|
||||
config.Domain = configuration.Domain
|
||||
|
||||
// Set the cookie SameSite option.
|
||||
switch configuration.SameSite {
|
||||
case "strict":
|
||||
config.CookieSameSite = fasthttp.CookieSameSiteStrictMode
|
||||
case "none":
|
||||
config.CookieSameSite = fasthttp.CookieSameSiteNoneMode
|
||||
case "lax":
|
||||
config.CookieSameSite = fasthttp.CookieSameSiteLaxMode
|
||||
default:
|
||||
config.CookieSameSite = fasthttp.CookieSameSiteLaxMode
|
||||
}
|
||||
|
||||
// Only serve the header over HTTPS.
|
||||
config.Secure = true
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/fasthttp/session/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/utils"
|
||||
|
@ -27,7 +28,6 @@ func TestShouldCreateInMemorySessionProvider(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -185,6 +185,28 @@ func TestShouldCreateRedisSentinelSessionProvider(t *testing.T) {
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue