summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.template.yml9
-rw-r--r--docs/configuration/session.md40
-rw-r--r--docs/security/measures.md16
-rw-r--r--internal/configuration/schema/session.go21
-rw-r--r--internal/configuration/validator/session.go19
-rw-r--r--internal/configuration/validator/session_test.go34
-rw-r--r--internal/handlers/handler_configuration.go2
-rw-r--r--internal/handlers/handler_configuration_test.go25
-rw-r--r--internal/handlers/handler_extended_configuration.go10
-rw-r--r--internal/handlers/handler_firstfactor.go11
-rw-r--r--internal/handlers/handler_verify.go2
-rw-r--r--internal/handlers/handler_verify_test.go3
-rw-r--r--internal/mocks/mock_authelia_ctx.go1
-rw-r--r--internal/session/provider.go9
-rw-r--r--internal/session/provider_config.go1
-rw-r--r--internal/session/provider_config_test.go3
-rw-r--r--internal/session/provider_test.go3
-rw-r--r--internal/suites/BypassAll/configuration.yml1
-rw-r--r--internal/suites/Docker/configuration.yml1
-rw-r--r--internal/suites/DuoPush/configuration.yml1
-rw-r--r--internal/suites/HAProxy/configuration.yml1
-rw-r--r--internal/suites/HighAvailability/configuration.yml1
-rw-r--r--internal/suites/LDAP/configuration.yml1
-rw-r--r--internal/suites/Mariadb/configuration.yml1
-rw-r--r--internal/suites/MySQL/configuration.yml1
-rw-r--r--internal/suites/NetworkACL/configuration.yml1
-rw-r--r--internal/suites/OneFactorOnly/configuration.yml1
-rw-r--r--internal/suites/Postgres/configuration.yml1
-rw-r--r--internal/suites/ShortTimeouts/configuration.yml1
-rw-r--r--internal/suites/Standalone/configuration.yml1
-rw-r--r--internal/suites/Traefik/configuration.yml1
-rw-r--r--internal/suites/Traefik2/configuration.yml1
-rw-r--r--internal/suites/example/kube/authelia/configs/configuration.yml1
-rw-r--r--internal/utils/const.go17
-rw-r--r--internal/utils/constants.go6
-rw-r--r--internal/utils/time.go48
-rw-r--r--internal/utils/time_test.go77
-rw-r--r--web/src/App.tsx2
-rw-r--r--web/src/models/Configuration.ts1
-rw-r--r--web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx30
-rw-r--r--web/src/views/LoginPortal/LoginPortal.tsx7
41 files changed, 366 insertions, 47 deletions
diff --git a/config.template.yml b/config.template.yml
index 55d5b2bb7..8d6e1d12f 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -256,7 +256,7 @@ session:
# The secret to encrypt the session data. This is only used with Redis.
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET
- secret: unsecure_session_secret
+ secret: insecure_session_secret
# The time in seconds before the cookie expires and session is reset.
expiration: 3600 # 1 hour
@@ -264,6 +264,13 @@ session:
# The inactivity time in seconds before the session is reset.
inactivity: 300 # 5 minutes
+ # The remember me duration.
+ # Value of 0 disables remember me.
+ # Value is in seconds, or duration notation. See: https://docs.authelia.com/configuration/session.html#duration-notation
+ # Longer periods are considered less secure because a stolen cookie will last longer giving attackers more time to spy
+ # or attack. Currently the default is 1M or 1 month.
+ 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.
diff --git a/docs/configuration/session.md b/docs/configuration/session.md
index 42b4e2cdc..1abc7f731 100644
--- a/docs/configuration/session.md
+++ b/docs/configuration/session.md
@@ -32,6 +32,13 @@ session:
# The inactivity time in seconds before the session is reset.
inactivity: 300 # 5 minutes
+ # The remember me duration.
+ # Value of 0 disables remember me.
+ # Value is in seconds, or duration notation. See: https://docs.authelia.com/configuration/session.html#duration-notation
+ # Longer periods are considered less secure because a stolen cookie will last longer giving attackers more time to spy
+ # or attack. Currently the default is 1M or 1 month.
+ remember_me_duration: 1M
+
# The domain to protect.
# Note: the login portal must also be a subdomain of that domain.
domain: example.com
@@ -43,4 +50,35 @@ session:
port: 6379
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD
password: authelia
-``` \ No newline at end of file
+```
+
+### Security
+
+Configuration of this section has an impact on security. You should read notes in
+[security measures](../security/measures.md#session-security) for more information.
+
+# Duration Notation
+
+We have implemented a string based notation for configuration options that take a duration. This section describes its
+usage.
+
+**NOTE:** At the time of this writing, only remember_me_duration uses this value type. But we plan to change expiration
+and inactivity.
+
+The notation is comprised of a number which must be positive and not have leading zeros, followed by a letter
+denoting the unit of time measurement. The table below describes the units of time and the associated letter.
+
+|Unit |Associated Letter|
+|:-----:|:---------------:|
+|Years |y |
+|Months |M |
+|Weeks |w |
+|Days |d |
+|Hours |h |
+|Minutes|m |
+|Seconds|s |
+
+Examples:
+* 1 hour and 30 minutes: 90m
+* 1 day: 1d
+* 10 hours: 10h \ No newline at end of file
diff --git a/docs/security/measures.md b/docs/security/measures.md
index b3956119b..48cd6bc16 100644
--- a/docs/security/measures.md
+++ b/docs/security/measures.md
@@ -95,7 +95,21 @@ There are a few reasons for the security measures implemented:
an attacker to intercept a link used to setup 2FA; which reduces security
3. Not validating the identity of the server allows man-in-the-middle attacks
-## More protections measures with Nginx
+## Additional security
+
+### Session security
+
+We have a few options to configure the security of a session. The main and most important
+one is the session secret. This is used to encrypt the session data when when stored in the
+Redis key value database. This should be as random as possible.
+
+Additionally you can configure the validity period of sessions. For example in a highly
+security conscious domain you would probably want to set the session remember_me_duration
+to 0 to disable this feature, and set an expiration of something like 2 hours and inactivity
+of 10 minutes. This means the hard limit or the time the session will be destroyed no matter
+what is 2 hours, and the soft limit or the time a user can be inactive for is 10 minutes.
+
+### More protections measures with Nginx
You can also apply the following headers to your nginx configuration for
improving security. Please read the documentation of those headers before
diff --git a/internal/configuration/schema/session.go b/internal/configuration/schema/session.go
index 797cce82f..268dfef07 100644
--- a/internal/configuration/schema/session.go
+++ b/internal/configuration/schema/session.go
@@ -10,18 +10,19 @@ type RedisSessionConfiguration struct {
// SessionConfiguration represents the configuration related to user sessions.
type SessionConfiguration struct {
- Name string `mapstructure:"name"`
- Secret string `mapstructure:"secret"`
- // Expiration in seconds
- Expiration int64 `mapstructure:"expiration"`
- // Inactivity in seconds
- Inactivity int64 `mapstructure:"inactivity"`
- Domain string `mapstructure:"domain"`
- Redis *RedisSessionConfiguration `mapstructure:"redis"`
+ // TODO(james-d-elliott): Convert to duration notation (Both Expiration and Activity need to be strings, and default needs to be changed)
+ Name string `mapstructure:"name"`
+ Secret string `mapstructure:"secret"`
+ Expiration int64 `mapstructure:"expiration"` // Expiration in seconds
+ Inactivity int64 `mapstructure:"inactivity"` // Inactivity in seconds
+ RememberMeDuration string `mapstructure:"remember_me_duration"`
+ Domain string `mapstructure:"domain"`
+ Redis *RedisSessionConfiguration `mapstructure:"redis"`
}
// DefaultSessionConfiguration is the default session configuration
var DefaultSessionConfiguration = SessionConfiguration{
- Name: "authelia_session",
- Expiration: 3600,
+ Name: "authelia_session",
+ Expiration: 3600,
+ RememberMeDuration: "1M",
}
diff --git a/internal/configuration/validator/session.go b/internal/configuration/validator/session.go
index d49ee185a..d78e917f4 100644
--- a/internal/configuration/validator/session.go
+++ b/internal/configuration/validator/session.go
@@ -2,8 +2,9 @@ package validator
import (
"errors"
-
+ "fmt"
"github.com/authelia/authelia/internal/configuration/schema"
+ "github.com/authelia/authelia/internal/utils"
)
// ValidateSession validates and update session configuration.
@@ -16,8 +17,24 @@ func ValidateSession(configuration *schema.SessionConfiguration, validator *sche
validator.Push(errors.New("Set secret of the session object"))
}
+ // TODO(james-d-elliott): Convert to duration notation
if configuration.Expiration == 0 {
configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour
+ } else if configuration.Expiration < 1 {
+ validator.Push(errors.New("Set expiration of the session above 0"))
+ }
+
+ // TODO(james-d-elliott): Convert to duration notation
+ if configuration.Inactivity < 0 {
+ validator.Push(errors.New("Set inactivity of the session to 0 or above"))
+ }
+
+ if configuration.RememberMeDuration == "" {
+ configuration.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
+ } else {
+ if _, err := utils.ParseDurationString(configuration.RememberMeDuration); err != nil {
+ validator.Push(errors.New(fmt.Sprintf("Error occurred parsing remember_me_duration string: %s", err)))
+ }
}
if configuration.Domain == "" {
diff --git a/internal/configuration/validator/session_test.go b/internal/configuration/validator/session_test.go
index 29a2b1eba..77f1ddddd 100644
--- a/internal/configuration/validator/session_test.go
+++ b/internal/configuration/validator/session_test.go
@@ -53,3 +53,37 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Set domain of the session object")
}
+
+func TestShouldRaiseErrorWhenBadInactivityAndExpirationSet(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := newDefaultSessionConfig()
+ config.Inactivity = -1
+ config.Expiration = -1
+
+ ValidateSession(&config, validator)
+
+ assert.Len(t, validator.Errors(), 2)
+ assert.EqualError(t, validator.Errors()[0], "Set expiration of the session above 0")
+ assert.EqualError(t, validator.Errors()[1], "Set inactivity of the session to 0 or above")
+}
+
+func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := newDefaultSessionConfig()
+ config.RememberMeDuration = "1 year"
+
+ ValidateSession(&config, validator)
+
+ assert.Len(t, validator.Errors(), 1)
+ assert.EqualError(t, validator.Errors()[0], "Error occurred parsing remember_me_duration string: could not convert the input string of 1 year into a duration")
+}
+
+func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
+ validator := schema.NewStructValidator()
+ config := newDefaultSessionConfig()
+
+ ValidateSession(&config, validator)
+
+ assert.Len(t, validator.Errors(), 0)
+ assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration)
+}
diff --git a/internal/handlers/handler_configuration.go b/internal/handlers/handler_configuration.go
index e4ae3d3f5..3a2b02cd3 100644
--- a/internal/handlers/handler_configuration.go
+++ b/internal/handlers/handler_configuration.go
@@ -4,11 +4,13 @@ import "github.com/authelia/authelia/internal/middlewares"
type ConfigurationBody struct {
GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
+ RememberMeEnabled bool `json:"remember_me_enabled"` // whether remember me is enabled or not
}
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
+ RememberMeEnabled: ctx.Providers.SessionProvider.RememberMe != 0,
}
ctx.SetJSONBody(body)
}
diff --git a/internal/handlers/handler_configuration_test.go b/internal/handlers/handler_configuration_test.go
index a01e42f61..a5189241c 100644
--- a/internal/handlers/handler_configuration_test.go
+++ b/internal/handlers/handler_configuration_test.go
@@ -1,8 +1,11 @@
package handlers
import (
+ "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/mocks"
+ "github.com/authelia/authelia/internal/session"
"github.com/stretchr/testify/suite"
+ "testing"
)
type ConfigurationSuite struct {
@@ -22,11 +25,33 @@ func (s *ConfigurationSuite) TearDownTest() {
func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
GATrackingID := "ABC"
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
+ s.mock.Ctx.Configuration.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID,
+ RememberMeEnabled: true,
}
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
+
+func (s *ConfigurationSuite) TestShouldDisableRememberMe() {
+ GATrackingID := "ABC"
+ s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
+ s.mock.Ctx.Configuration.Session.RememberMeDuration = "0"
+ s.mock.Ctx.Providers.SessionProvider = session.NewProvider(
+ s.mock.Ctx.Configuration.Session)
+ expectedBody := ConfigurationBody{
+ GoogleAnalyticsTrackingID: GATrackingID,
+ RememberMeEnabled: false,
+ }
+
+ ConfigurationGet(s.mock.Ctx)
+ s.mock.Assert200OK(s.T(), expectedBody)
+}
+
+func TestRunHandlerConfigurationSuite(t *testing.T) {
+ s := new(ConfigurationSuite)
+ suite.Run(t, s)
+}
diff --git a/internal/handlers/handler_extended_configuration.go b/internal/handlers/handler_extended_configuration.go
index 0ac352a70..052798670 100644
--- a/internal/handlers/handler_extended_configuration.go
+++ b/internal/handlers/handler_extended_configuration.go
@@ -7,13 +7,9 @@ import (
// ExtendedConfigurationBody the content returned by extended configuration endpoint
type ExtendedConfigurationBody struct {
- AvailableMethods MethodList `json:"available_methods"`
-
- // SecondFactorEnabled whether second factor is enabled
- SecondFactorEnabled bool `json:"second_factor_enabled"`
-
- // TOTP Period
- TOTPPeriod int `json:"totp_period"`
+ AvailableMethods MethodList `json:"available_methods"`
+ SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not
+ TOTPPeriod int `json:"totp_period"`
}
// ExtendedConfigurationGet get the extended configuration accessible to authenticated users.
diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go
index 00c11cb69..c7a1a57fe 100644
--- a/internal/handlers/handler_firstfactor.go
+++ b/internal/handlers/handler_firstfactor.go
@@ -74,9 +74,12 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
return
}
- // set the cookie to expire in 1 year if "Remember me" was ticked.
- if *bodyJSON.KeepMeLoggedIn {
- err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, time.Duration(31556952*time.Second))
+ // Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data
+ keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != 0 && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn
+
+ // Set the cookie to expire if remember me is enabled and the user has asked us to
+ if keepMeLoggedIn {
+ err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, ctx.Providers.SessionProvider.RememberMe)
if err != nil {
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
return
@@ -100,7 +103,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
userSession.Emails = userDetails.Emails
userSession.AuthenticationLevel = authentication.OneFactor
userSession.LastActivity = time.Now().Unix()
- userSession.KeepMeLoggedIn = *bodyJSON.KeepMeLoggedIn
+ userSession.KeepMeLoggedIn = keepMeLoggedIn
err = ctx.SaveSession(userSession)
if err != nil {
diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go
index 4b152420a..aafa2667d 100644
--- a/internal/handlers/handler_verify.go
+++ b/internal/handlers/handler_verify.go
@@ -154,6 +154,8 @@ func setForwardedHeaders(headers *fasthttp.ResponseHeader, username string, grou
// hasUserBeenInactiveLongEnough check whether the user has been inactive for too long.
func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) {
+
+ // TODO(james-d-elliott): Convert to duration notation
maxInactivityPeriod := ctx.Configuration.Session.Inactivity
if maxInactivityPeriod == 0 {
return false, nil
diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go
index eaf193920..f65dbbd0c 100644
--- a/internal/handlers/handler_verify_test.go
+++ b/internal/handlers/handler_verify_test.go
@@ -469,6 +469,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
clock := mocks.TestingClock{}
clock.Set(time.Now())
+ // TODO(james-d-elliott): Convert to duration notation
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
@@ -494,6 +495,7 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te
clock := mocks.TestingClock{}
clock.Set(time.Now())
+ // TODO(james-d-elliott): Convert to duration notation
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
@@ -520,6 +522,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T)
clock := mocks.TestingClock{}
clock.Set(time.Now())
+ // TODO(james-d-elliott): Convert to duration notation
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
diff --git a/internal/mocks/mock_authelia_ctx.go b/internal/mocks/mock_authelia_ctx.go
index d91facc59..2037a19b7 100644
--- a/internal/mocks/mock_authelia_ctx.go
+++ b/internal/mocks/mock_authelia_ctx.go
@@ -67,6 +67,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia.Clock.Set(datetime)
configuration := schema.Configuration{}
+ configuration.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
configuration.Session.Name = "authelia_session"
configuration.AccessControl.DefaultPolicy = "deny"
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
diff --git a/internal/session/provider.go b/internal/session/provider.go
index f61b12ad4..c321d6298 100644
--- a/internal/session/provider.go
+++ b/internal/session/provider.go
@@ -2,6 +2,7 @@ package session
import (
"encoding/json"
+ "github.com/authelia/authelia/internal/utils"
"time"
"github.com/authelia/authelia/internal/configuration/schema"
@@ -12,6 +13,7 @@ import (
// Provider a session provider.
type Provider struct {
sessionHolder *fasthttpsession.Session
+ RememberMe time.Duration
}
// NewProvider instantiate a session provider given a configuration.
@@ -20,7 +22,12 @@ func NewProvider(configuration schema.SessionConfiguration) *Provider {
provider := new(Provider)
provider.sessionHolder = fasthttpsession.New(providerConfig.config)
- err := provider.sessionHolder.SetProvider(providerConfig.providerName, providerConfig.providerConfig)
+ duration, err := utils.ParseDurationString(configuration.RememberMeDuration)
+ if err != nil {
+ panic(err)
+ }
+ provider.RememberMe = duration
+ err = provider.sessionHolder.SetProvider(providerConfig.providerName, providerConfig.providerConfig)
if err != nil {
panic(err)
}
diff --git a/internal/session/provider_config.go b/internal/session/provider_config.go
index ea33a8bc3..8d176ad15 100644
--- a/internal/session/provider_config.go
+++ b/internal/session/provider_config.go
@@ -24,6 +24,7 @@ func NewProviderConfig(configuration schema.SessionConfiguration) ProviderConfig
// Only serve the header over HTTPS.
config.Secure = true
+ // TODO(james-d-elliott): Convert to duration notation
if configuration.Expiration > 0 {
config.Expires = time.Duration(configuration.Expiration) * time.Second
} else {
diff --git a/internal/session/provider_config_test.go b/internal/session/provider_config_test.go
index 0741deb3e..1f1c7d863 100644
--- a/internal/session/provider_config_test.go
+++ b/internal/session/provider_config_test.go
@@ -19,6 +19,7 @@ func TestShouldCreateInMemorySessionProvider(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
providerConfig := NewProviderConfig(configuration)
@@ -37,6 +38,7 @@ func TestShouldCreateRedisSessionProvider(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
configuration.Redis = &schema.RedisSessionConfiguration{
Host: "redis.example.com",
@@ -66,6 +68,7 @@ func TestShouldSetDbNumber(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
configuration.Redis = &schema.RedisSessionConfiguration{
Host: "redis.example.com",
diff --git a/internal/session/provider_test.go b/internal/session/provider_test.go
index f7f1b6835..c136d874d 100644
--- a/internal/session/provider_test.go
+++ b/internal/session/provider_test.go
@@ -18,6 +18,7 @@ func TestShouldInitializerSession(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
provider := NewProvider(configuration)
@@ -32,6 +33,7 @@ func TestShouldUpdateSession(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
provider := NewProvider(configuration)
@@ -57,6 +59,7 @@ func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
+ // TODO(james-d-elliott): Convert to duration notation
configuration.Expiration = 40
provider := NewProvider(configuration)
diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml
index de4fe375c..2b956f9c3 100644
--- a/internal/suites/BypassAll/configuration.yml
+++ b/internal/suites/BypassAll/configuration.yml
@@ -17,6 +17,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml
index 0b1abef1d..76d8ec4ee 100644
--- a/internal/suites/Docker/configuration.yml
+++ b/internal/suites/Docker/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml
index 028d3df7d..5bb130d92 100644
--- a/internal/suites/DuoPush/configuration.yml
+++ b/internal/suites/DuoPush/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/HAProxy/configuration.yml b/internal/suites/HAProxy/configuration.yml
index 54283cd34..cf85a43c5 100644
--- a/internal/suites/HAProxy/configuration.yml
+++ b/internal/suites/HAProxy/configuration.yml
@@ -17,6 +17,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml
index 6b211f421..c82ddab75 100644
--- a/internal/suites/HighAvailability/configuration.yml
+++ b/internal/suites/HighAvailability/configuration.yml
@@ -85,6 +85,7 @@ session:
host: redis
port: 6379
password: authelia
+ remember_me_duration: 1y
regulation:
max_retries: 3
diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml
index 5570ed954..d7a51cfae 100644
--- a/internal/suites/LDAP/configuration.yml
+++ b/internal/suites/LDAP/configuration.yml
@@ -30,6 +30,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Mariadb/configuration.yml b/internal/suites/Mariadb/configuration.yml
index c67052bc1..dadfc6e4d 100644
--- a/internal/suites/Mariadb/configuration.yml
+++ b/internal/suites/Mariadb/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/MySQL/configuration.yml b/internal/suites/MySQL/configuration.yml
index 7d521eea8..447bfd128 100644
--- a/internal/suites/MySQL/configuration.yml
+++ b/internal/suites/MySQL/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/NetworkACL/configuration.yml b/internal/suites/NetworkACL/configuration.yml
index e10b8d297..3da03b5ab 100644
--- a/internal/suites/NetworkACL/configuration.yml
+++ b/internal/suites/NetworkACL/configuration.yml
@@ -17,6 +17,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/OneFactorOnly/configuration.yml b/internal/suites/OneFactorOnly/configuration.yml
index 5ff1e87fe..cb791f2ca 100644
--- a/internal/suites/OneFactorOnly/configuration.yml
+++ b/internal/suites/OneFactorOnly/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml
index a33794a2a..aff76b0a1 100644
--- a/internal/suites/Postgres/configuration.yml
+++ b/internal/suites/Postgres/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml
index 05cdb71cb..6446ebf9b 100644
--- a/internal/suites/ShortTimeouts/configuration.yml
+++ b/internal/suites/ShortTimeouts/configuration.yml
@@ -19,6 +19,7 @@ session:
domain: example.com
inactivity: 5
expiration: 8
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml
index 91aa67dec..8e7dc51e7 100644
--- a/internal/suites/Standalone/configuration.yml
+++ b/internal/suites/Standalone/configuration.yml
@@ -16,6 +16,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml
index 1c849b8ea..154ba83aa 100644
--- a/internal/suites/Traefik/configuration.yml
+++ b/internal/suites/Traefik/configuration.yml
@@ -17,6 +17,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml
index 1c849b8ea..154ba83aa 100644
--- a/internal/suites/Traefik2/configuration.yml
+++ b/internal/suites/Traefik2/configuration.yml
@@ -17,6 +17,7 @@ session:
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
storage:
local:
diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml
index 9e1a75280..20eb1f481 100644
--- a/internal/suites/example/kube/authelia/configs/configuration.yml
+++ b/internal/suites/example/kube/authelia/configs/configuration.yml
@@ -75,6 +75,7 @@ access_control:
session:
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
+ remember_me_duration: 1y
domain: example.com
redis:
host: redis-service
diff --git a/internal/utils/const.go b/internal/utils/const.go
new file mode 100644
index 000000000..05ffb344c
--- /dev/null
+++ b/internal/utils/const.go
@@ -0,0 +1,17 @@
+package utils
+
+import (
+ "errors"
+ "regexp"
+ "time"
+)
+
+// ErrTimeoutReached error thrown when a timeout is reached
+var ErrTimeoutReached = errors.New("timeout reached")
+var parseDurationRegexp = regexp.MustCompile(`^(?P<Duration>[1-9]\d*?)(?P<Unit>[smhdwMy])?$`)
+
+const Hour = time.Minute * 60
+const Day = Hour * 24
+const Week = Day * 7
+const Year = Day * 365
+const Month = Year / 12
diff --git a/internal/utils/constants.go b/internal/utils/constants.go
deleted file mode 100644
index bb19fd55a..000000000
--- a/internal/utils/constants.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package utils
-
-import "errors"
-
-// ErrTimeoutReached error thrown when a timeout is reached
-var ErrTimeoutReached = errors.New("timeout reached")
diff --git a/internal/utils/time.go b/internal/utils/time.go
new file mode 100644
index 000000000..bb513c2e0
--- /dev/null
+++ b/internal/utils/time.go
@@ -0,0 +1,48 @@
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "time"
+)
+
+// Parses a string to a duration
+// Duration notations are an integer followed by a unit
+// Units are s = second, m = minute, d = day, w = week, M = month, y = year
+// Example 1y is the same as 1 year
+func ParseDurationString(input string) (duration time.Duration, err error) {
+ duration = 0
+ err = nil
+ matches := parseDurationRegexp.FindStringSubmatch(input)
+ if len(matches) == 3 && matches[2] != "" {
+ d, _ := strconv.Atoi(matches[1])
+ switch matches[2] {
+ case "y":
+ duration = time.Duration(d) * Year
+ case "M":
+ duration = time.Duration(d) * Month
+ case "w":
+ duration = time.Duration(d) * Week
+ case "d":
+ duration = time.Duration(d) * Day
+ case "h":
+ duration = time.Duration(d) * Hour
+ case "m":
+ duration = time.Duration(d) * time.Minute
+ case "s":
+ duration = time.Duration(d) * time.Second
+ }
+ } else if input == "0" || len(matches) == 3 {
+ seconds, err := strconv.Atoi(input)
+ if err != nil {
+ err = errors.New(fmt.Sprintf("could not convert the input string of %s into a duration: %s", input, err))
+ } else {
+ duration = time.Duration(seconds) * time.Second
+ }
+ } else if input != "" {
+ // Throw this error if input is anything other than a blank string, blank string will default to a duration of nothing
+ err = errors.New(fmt.Sprintf("could not convert the input string of %s into a duration", input))
+ }
+ return
+}
diff --git a/internal/utils/time_test.go b/internal/utils/time_test.go
new file mode 100644
index 000000000..a589d6d88
--- /dev/null
+++ b/internal/utils/time_test.go
@@ -0,0 +1,77 @@
+package utils
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestShouldParseDurationString(t *testing.T) {
+ duration, err := ParseDurationString("1h")
+ assert.NoError(t, err)
+ assert.Equal(t, 60*time.Minute, duration)
+}
+
+func TestShouldParseDurationStringAllUnits(t *testing.T) {
+ duration, err := ParseDurationString("1y")
+ assert.NoError(t, err)
+ assert.Equal(t, Year, duration)
+
+ duration, err = ParseDurationString("1M")
+ assert.NoError(t, err)
+ assert.Equal(t, Month, duration)
+
+ duration, err = ParseDurationString("1w")
+ assert.NoError(t, err)
+ assert.Equal(t, Week, duration)
+
+ duration, err = ParseDurationString("1d")
+ assert.NoError(t, err)
+ assert.Equal(t, Day, duration)
+
+ duration, err = ParseDurationString("1h")
+ assert.NoError(t, err)
+ assert.Equal(t, Hour, duration)
+
+ duration, err = ParseDurationString("1s")
+ assert.NoError(t, err)
+ assert.Equal(t, time.Second, duration)
+}
+
+func TestShouldParseSecondsString(t *testing.T) {
+ duration, err := ParseDurationString("100")
+ assert.NoError(t, err)
+ assert.Equal(t, 100*time.Second, duration)
+}
+
+func TestShouldNotParseDurationStringWithOutOfOrderQuantitiesAndUnits(t *testing.T) {
+ duration, err := ParseDurationString("h1")
+ assert.EqualError(t, err, "could not convert the input string of h1 into a duration")
+ assert.Equal(t, time.Duration(0), duration)
+}
+
+func TestShouldNotParseBadDurationString(t *testing.T) {
+ duration, err := ParseDurationString("10x")
+ assert.EqualError(t, err, "could not convert the input string of 10x into a duration")
+ assert.Equal(t, time.Duration(0), duration)
+}
+
+func TestShouldNotParseDurationStringWithMultiValueUnits(t *testing.T) {
+ duration, err := ParseDurationString("10ms")
+ assert.EqualError(t, err, "could not convert the input string of 10ms into a duration")
+ assert.Equal(t, time.Duration(0), duration)
+}
+
+func TestShouldNotParseDurationStringWithLeadingZero(t *testing.T) {
+ duration, err := ParseDurationString("005h")
+ assert.EqualError(t, err, "could not convert the input string of 005h into a duration")
+ assert.Equal(t, time.Duration(0), duration)
+}
+
+func TestShouldTimeIntervalsMakeSense(t *testing.T) {
+ assert.Equal(t, Hour, time.Minute*60)
+ assert.Equal(t, Day, Hour*24)
+ assert.Equal(t, Week, Day*7)
+ assert.Equal(t, Year, Day*365)
+ assert.Equal(t, Month, Year/12)
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 83afe3db1..7d9135873 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -56,7 +56,7 @@ const App: React.FC = () => {
<SignOut />
</Route>
<Route path={FirstFactorRoute}>
- <LoginPortal />
+ <LoginPortal rememberMe={configuration?.remember_me_enabled === true}/>
</Route>
<Route path="/">
<Redirect to={FirstFactorRoute}></Redirect>
diff --git a/web/src/models/Configuration.ts b/web/src/models/Configuration.ts
index 709efa604..c57dd9898 100644
--- a/web/src/models/Configuration.ts
+++ b/web/src/models/Configuration.ts
@@ -2,6 +2,7 @@ import { SecondFactorMethod } from "./Methods";
export interface Configuration {
ga_tracking_id: string;
+ remember_me_enabled: boolean;
}
export interface ExtendedConfiguration {
diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
index f3825a933..a9f1636b0 100644
--- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
+++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
@@ -11,6 +11,7 @@ import FixedTextField from "../../../components/FixedTextField";
export interface Props {
disabled: boolean;
+ rememberMe: boolean;
onAuthenticationStart: () => void;
onAuthenticationFailure: () => void;
@@ -121,19 +122,20 @@ export default function (props: Props) {
}} />
</Grid>
<Grid item xs={12} className={classnames(style.leftAlign, style.actionRow)}>
- <FormControlLabel
- control={
- <Checkbox
- id="remember-checkbox"
- disabled={disabled}
- checked={rememberMe}
- onChange={handleRememberMeChange}
- value="rememberMe"
- color="primary" />
- }
- className={style.rememberMe}
- label="Remember me"
- />
+ {props.rememberMe ?
+ <FormControlLabel
+ control={
+ <Checkbox
+ id="remember-checkbox"
+ disabled={disabled}
+ checked={rememberMe}
+ onChange={handleRememberMeChange}
+ value="rememberMe"
+ color="primary"/>
+ }
+ className={style.rememberMe}
+ label="Remember me"
+ /> : null}
<Link
id="reset-password-button"
component="button"
@@ -171,6 +173,8 @@ const useStyles = makeStyles(theme => ({
},
resetLink: {
cursor: "pointer",
+ paddingTop: 13.5,
+ paddingBottom: 13.5,
},
rememberMe: {
flexGrow: 1,
diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx
index bc95d3521..91ca68847 100644
--- a/web/src/views/LoginPortal/LoginPortal.tsx
+++ b/web/src/views/LoginPortal/LoginPortal.tsx
@@ -16,7 +16,11 @@ import { SecondFactorMethod } from "../../models/Methods";
import { useExtendedConfiguration } from "../../hooks/Configuration";
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
-export default function () {
+export interface Props {
+ rememberMe: boolean;
+}
+
+export default function (props: Props) {
const history = useHistory();
const location = useLocation();
const redirectionURL = useRedirectionURL();
@@ -114,6 +118,7 @@ export default function () {
<ComponentOrLoading ready={firstFactorReady}>
<FirstFactorForm
disabled={firstFactorDisabled}
+ rememberMe={props.rememberMe}
onAuthenticationStart={() => setFirstFactorDisabled(true)}
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
onAuthenticationSuccess={handleAuthSuccess} />