[FEATURE] Remember Me Configuration (#813)
* [FEATURE] Remember Me Configuration * allow users to specify the duration of remember me using remember_me_duration in session config * setting the duration to 0 disables remember me * only render the remember me element if remember me is enabled * prevent malicious users from faking remember me functionality in the backend * add string to duration helper called ParseDurationString to parse a string into a duration * added tests to the helper function * use the SessionProvider to store the time.Duration instead of parsing it over and over again * add sec doc, adjust month/min, consistency * renamed internal/utils/constants.go to internal/utils/const.go to be consistent * added security measure docs * adjusted default remember me duration to be 1 month instead of 1 year * utilize default remember me duration in the autheliaCtx mock * adjust order of keys in session configuration examples * add notes on session security measures secret only being redis * add TODO items for duration notation for both Expiration and Inactivity (will be removed soon) * fix error text for Inactivity in the validator * add session validator tests * deref check bodyJSON.KeepMeLoggedIn and derive the value based on conf and user input and store it (DRY) * remove unnecessary regex for the simplified ParseDurationString utility * ParseDurationString only accepts decimals without leading zeros now * comprehensively test all unit types * remove unnecessary type unions in web * add test to check sanity of time duration consts, this is just so they can't be accidentally changed * simplify deref check and assignment * fix reset password padding/margins * adjust some doc wording * adjust the handler configuration suite test * actually run the handler configuration suite test (whoops) * reduce the number of regex's used by ParseDurationString to 1, thanks to Clement * adjust some error wordingpull/825/head
parent
4fcaff7c4b
commit
626f5d2949
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
```
|
||||
```
|
||||
|
||||
### 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
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -17,6 +17,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -19,6 +19,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -17,6 +17,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -85,6 +85,7 @@ session:
|
|||
host: redis
|
||||
port: 6379
|
||||
password: authelia
|
||||
remember_me_duration: 1y
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
|
|
|
@ -30,6 +30,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -19,6 +19,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -19,6 +19,7 @@ session:
|
|||
domain: example.com
|
||||
inactivity: 5
|
||||
expiration: 8
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -16,6 +16,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -17,6 +17,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -17,6 +17,7 @@ session:
|
|||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
local:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
package utils
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrTimeoutReached error thrown when a timeout is reached
|
||||
var ErrTimeoutReached = errors.New("timeout reached")
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { SecondFactorMethod } from "./Methods";
|
|||
|
||||
export interface Configuration {
|
||||
ga_tracking_id: string;
|
||||
remember_me_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ExtendedConfiguration {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />
|
||||
|
|
Loading…
Reference in New Issue