[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.
|
# 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
|
# 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.
|
# The time in seconds before the cookie expires and session is reset.
|
||||||
expiration: 3600 # 1 hour
|
expiration: 3600 # 1 hour
|
||||||
|
@ -264,6 +264,13 @@ session:
|
||||||
# The inactivity time in seconds before the session is reset.
|
# The inactivity time in seconds before the session is reset.
|
||||||
inactivity: 300 # 5 minutes
|
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.
|
# The domain to protect.
|
||||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||||
# is restricted to the subdomain of the issuer.
|
# is restricted to the subdomain of the issuer.
|
||||||
|
|
|
@ -32,6 +32,13 @@ session:
|
||||||
# The inactivity time in seconds before the session is reset.
|
# The inactivity time in seconds before the session is reset.
|
||||||
inactivity: 300 # 5 minutes
|
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.
|
# The domain to protect.
|
||||||
# Note: the login portal must also be a subdomain of that domain.
|
# Note: the login portal must also be a subdomain of that domain.
|
||||||
domain: example.com
|
domain: example.com
|
||||||
|
@ -44,3 +51,34 @@ session:
|
||||||
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD
|
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD
|
||||||
password: authelia
|
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
|
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
|
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
|
You can also apply the following headers to your nginx configuration for
|
||||||
improving security. Please read the documentation of those headers before
|
improving security. Please read the documentation of those headers before
|
||||||
|
|
|
@ -10,12 +10,12 @@ 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 {
|
||||||
|
// 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"`
|
Name string `mapstructure:"name"`
|
||||||
Secret string `mapstructure:"secret"`
|
Secret string `mapstructure:"secret"`
|
||||||
// Expiration in seconds
|
Expiration int64 `mapstructure:"expiration"` // Expiration in seconds
|
||||||
Expiration int64 `mapstructure:"expiration"`
|
Inactivity int64 `mapstructure:"inactivity"` // Inactivity in seconds
|
||||||
// Inactivity in seconds
|
RememberMeDuration string `mapstructure:"remember_me_duration"`
|
||||||
Inactivity int64 `mapstructure:"inactivity"`
|
|
||||||
Domain string `mapstructure:"domain"`
|
Domain string `mapstructure:"domain"`
|
||||||
Redis *RedisSessionConfiguration `mapstructure:"redis"`
|
Redis *RedisSessionConfiguration `mapstructure:"redis"`
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,5 @@ type SessionConfiguration struct {
|
||||||
var DefaultSessionConfiguration = SessionConfiguration{
|
var DefaultSessionConfiguration = SessionConfiguration{
|
||||||
Name: "authelia_session",
|
Name: "authelia_session",
|
||||||
Expiration: 3600,
|
Expiration: 3600,
|
||||||
|
RememberMeDuration: "1M",
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@ package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateSession validates and update session configuration.
|
// 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"))
|
validator.Push(errors.New("Set secret of the session object"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
if configuration.Expiration == 0 {
|
if configuration.Expiration == 0 {
|
||||||
configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour
|
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 == "" {
|
if configuration.Domain == "" {
|
||||||
|
|
|
@ -53,3 +53,37 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
|
||||||
assert.Len(t, validator.Errors(), 1)
|
assert.Len(t, validator.Errors(), 1)
|
||||||
assert.EqualError(t, validator.Errors()[0], "Set domain of the session object")
|
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 {
|
type ConfigurationBody struct {
|
||||||
GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
|
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) {
|
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||||
body := ConfigurationBody{
|
body := ConfigurationBody{
|
||||||
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
|
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
|
||||||
|
RememberMeEnabled: ctx.Providers.SessionProvider.RememberMe != 0,
|
||||||
}
|
}
|
||||||
ctx.SetJSONBody(body)
|
ctx.SetJSONBody(body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/internal/mocks"
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigurationSuite struct {
|
type ConfigurationSuite struct {
|
||||||
|
@ -22,11 +25,33 @@ func (s *ConfigurationSuite) TearDownTest() {
|
||||||
func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
|
func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
|
||||||
GATrackingID := "ABC"
|
GATrackingID := "ABC"
|
||||||
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
|
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
|
||||||
|
s.mock.Ctx.Configuration.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
|
||||||
|
|
||||||
expectedBody := ConfigurationBody{
|
expectedBody := ConfigurationBody{
|
||||||
GoogleAnalyticsTrackingID: GATrackingID,
|
GoogleAnalyticsTrackingID: GATrackingID,
|
||||||
|
RememberMeEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationGet(s.mock.Ctx)
|
ConfigurationGet(s.mock.Ctx)
|
||||||
s.mock.Assert200OK(s.T(), expectedBody)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -8,11 +8,7 @@ import (
|
||||||
// ExtendedConfigurationBody the content returned by extended configuration endpoint
|
// ExtendedConfigurationBody the content returned by extended configuration endpoint
|
||||||
type ExtendedConfigurationBody struct {
|
type ExtendedConfigurationBody struct {
|
||||||
AvailableMethods MethodList `json:"available_methods"`
|
AvailableMethods MethodList `json:"available_methods"`
|
||||||
|
SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not
|
||||||
// SecondFactorEnabled whether second factor is enabled
|
|
||||||
SecondFactorEnabled bool `json:"second_factor_enabled"`
|
|
||||||
|
|
||||||
// TOTP Period
|
|
||||||
TOTPPeriod int `json:"totp_period"`
|
TOTPPeriod int `json:"totp_period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,9 +74,12 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the cookie to expire in 1 year if "Remember me" was ticked.
|
// Check if bodyJSON.KeepMeLoggedIn can be deref'd and derive the value based on the configuration and JSON data
|
||||||
if *bodyJSON.KeepMeLoggedIn {
|
keepMeLoggedIn := ctx.Providers.SessionProvider.RememberMe != 0 && bodyJSON.KeepMeLoggedIn != nil && *bodyJSON.KeepMeLoggedIn
|
||||||
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, time.Duration(31556952*time.Second))
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
||||||
return
|
return
|
||||||
|
@ -100,7 +103,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession.Emails = userDetails.Emails
|
userSession.Emails = userDetails.Emails
|
||||||
userSession.AuthenticationLevel = authentication.OneFactor
|
userSession.AuthenticationLevel = authentication.OneFactor
|
||||||
userSession.LastActivity = time.Now().Unix()
|
userSession.LastActivity = time.Now().Unix()
|
||||||
userSession.KeepMeLoggedIn = *bodyJSON.KeepMeLoggedIn
|
userSession.KeepMeLoggedIn = keepMeLoggedIn
|
||||||
err = ctx.SaveSession(userSession)
|
err = ctx.SaveSession(userSession)
|
||||||
|
|
||||||
if err != nil {
|
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.
|
// hasUserBeenInactiveLongEnough check whether the user has been inactive for too long.
|
||||||
func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) {
|
func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) {
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
maxInactivityPeriod := ctx.Configuration.Session.Inactivity
|
maxInactivityPeriod := ctx.Configuration.Session.Inactivity
|
||||||
if maxInactivityPeriod == 0 {
|
if maxInactivityPeriod == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
|
@ -469,6 +469,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
||||||
clock := mocks.TestingClock{}
|
clock := mocks.TestingClock{}
|
||||||
clock.Set(time.Now())
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
mock.Ctx.Configuration.Session.Inactivity = 10
|
mock.Ctx.Configuration.Session.Inactivity = 10
|
||||||
|
|
||||||
userSession := mock.Ctx.GetSession()
|
userSession := mock.Ctx.GetSession()
|
||||||
|
@ -494,6 +495,7 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te
|
||||||
clock := mocks.TestingClock{}
|
clock := mocks.TestingClock{}
|
||||||
clock.Set(time.Now())
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
mock.Ctx.Configuration.Session.Inactivity = 10
|
mock.Ctx.Configuration.Session.Inactivity = 10
|
||||||
|
|
||||||
userSession := mock.Ctx.GetSession()
|
userSession := mock.Ctx.GetSession()
|
||||||
|
@ -520,6 +522,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T)
|
||||||
clock := mocks.TestingClock{}
|
clock := mocks.TestingClock{}
|
||||||
clock.Set(time.Now())
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
mock.Ctx.Configuration.Session.Inactivity = 10
|
mock.Ctx.Configuration.Session.Inactivity = 10
|
||||||
|
|
||||||
userSession := mock.Ctx.GetSession()
|
userSession := mock.Ctx.GetSession()
|
||||||
|
|
|
@ -67,6 +67,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
mockAuthelia.Clock.Set(datetime)
|
mockAuthelia.Clock.Set(datetime)
|
||||||
|
|
||||||
configuration := schema.Configuration{}
|
configuration := schema.Configuration{}
|
||||||
|
configuration.Session.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration
|
||||||
configuration.Session.Name = "authelia_session"
|
configuration.Session.Name = "authelia_session"
|
||||||
configuration.AccessControl.DefaultPolicy = "deny"
|
configuration.AccessControl.DefaultPolicy = "deny"
|
||||||
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
|
configuration.AccessControl.Rules = []schema.ACLRule{schema.ACLRule{
|
||||||
|
|
|
@ -2,6 +2,7 @@ package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
// Provider a session provider.
|
// Provider a session provider.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
sessionHolder *fasthttpsession.Session
|
sessionHolder *fasthttpsession.Session
|
||||||
|
RememberMe time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProvider instantiate a session provider given a configuration.
|
// NewProvider instantiate a session provider given a configuration.
|
||||||
|
@ -20,7 +22,12 @@ func NewProvider(configuration schema.SessionConfiguration) *Provider {
|
||||||
|
|
||||||
provider := new(Provider)
|
provider := new(Provider)
|
||||||
provider.sessionHolder = fasthttpsession.New(providerConfig.config)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ func NewProviderConfig(configuration schema.SessionConfiguration) ProviderConfig
|
||||||
// Only serve the header over HTTPS.
|
// Only serve the header over HTTPS.
|
||||||
config.Secure = true
|
config.Secure = true
|
||||||
|
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
if configuration.Expiration > 0 {
|
if configuration.Expiration > 0 {
|
||||||
config.Expires = time.Duration(configuration.Expiration) * time.Second
|
config.Expires = time.Duration(configuration.Expiration) * time.Second
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,6 +19,7 @@ func TestShouldCreateInMemorySessionProvider(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
providerConfig := NewProviderConfig(configuration)
|
providerConfig := NewProviderConfig(configuration)
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ func TestShouldCreateRedisSessionProvider(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
configuration.Redis = &schema.RedisSessionConfiguration{
|
configuration.Redis = &schema.RedisSessionConfiguration{
|
||||||
Host: "redis.example.com",
|
Host: "redis.example.com",
|
||||||
|
@ -66,6 +68,7 @@ func TestShouldSetDbNumber(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
configuration.Redis = &schema.RedisSessionConfiguration{
|
configuration.Redis = &schema.RedisSessionConfiguration{
|
||||||
Host: "redis.example.com",
|
Host: "redis.example.com",
|
||||||
|
|
|
@ -18,6 +18,7 @@ func TestShouldInitializerSession(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
|
|
||||||
provider := NewProvider(configuration)
|
provider := NewProvider(configuration)
|
||||||
|
@ -32,6 +33,7 @@ func TestShouldUpdateSession(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
|
|
||||||
provider := NewProvider(configuration)
|
provider := NewProvider(configuration)
|
||||||
|
@ -57,6 +59,7 @@ func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
configuration.Domain = "example.com"
|
configuration.Domain = "example.com"
|
||||||
configuration.Name = "my_session"
|
configuration.Name = "my_session"
|
||||||
|
// TODO(james-d-elliott): Convert to duration notation
|
||||||
configuration.Expiration = 40
|
configuration.Expiration = 40
|
||||||
|
|
||||||
provider := NewProvider(configuration)
|
provider := NewProvider(configuration)
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -85,6 +85,7 @@ session:
|
||||||
host: redis
|
host: redis
|
||||||
port: 6379
|
port: 6379
|
||||||
password: authelia
|
password: authelia
|
||||||
|
remember_me_duration: 1y
|
||||||
|
|
||||||
regulation:
|
regulation:
|
||||||
max_retries: 3
|
max_retries: 3
|
||||||
|
|
|
@ -30,6 +30,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -19,6 +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
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -19,6 +19,7 @@ session:
|
||||||
domain: example.com
|
domain: example.com
|
||||||
inactivity: 5
|
inactivity: 5
|
||||||
expiration: 8
|
expiration: 8
|
||||||
|
remember_me_duration: 1y
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -16,6 +16,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -17,6 +17,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
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
local:
|
local:
|
||||||
|
|
|
@ -75,6 +75,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
|
||||||
domain: example.com
|
domain: example.com
|
||||||
redis:
|
redis:
|
||||||
host: redis-service
|
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 />
|
<SignOut />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={FirstFactorRoute}>
|
<Route path={FirstFactorRoute}>
|
||||||
<LoginPortal />
|
<LoginPortal rememberMe={configuration?.remember_me_enabled === true}/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Redirect to={FirstFactorRoute}></Redirect>
|
<Redirect to={FirstFactorRoute}></Redirect>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { SecondFactorMethod } from "./Methods";
|
||||||
|
|
||||||
export interface Configuration {
|
export interface Configuration {
|
||||||
ga_tracking_id: string;
|
ga_tracking_id: string;
|
||||||
|
remember_me_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtendedConfiguration {
|
export interface ExtendedConfiguration {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import FixedTextField from "../../../components/FixedTextField";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
rememberMe: boolean;
|
||||||
|
|
||||||
onAuthenticationStart: () => void;
|
onAuthenticationStart: () => void;
|
||||||
onAuthenticationFailure: () => void;
|
onAuthenticationFailure: () => void;
|
||||||
|
@ -121,6 +122,7 @@ export default function (props: Props) {
|
||||||
}} />
|
}} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={classnames(style.leftAlign, style.actionRow)}>
|
<Grid item xs={12} className={classnames(style.leftAlign, style.actionRow)}>
|
||||||
|
{props.rememberMe ?
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -129,11 +131,11 @@ export default function (props: Props) {
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onChange={handleRememberMeChange}
|
onChange={handleRememberMeChange}
|
||||||
value="rememberMe"
|
value="rememberMe"
|
||||||
color="primary" />
|
color="primary"/>
|
||||||
}
|
}
|
||||||
className={style.rememberMe}
|
className={style.rememberMe}
|
||||||
label="Remember me"
|
label="Remember me"
|
||||||
/>
|
/> : null}
|
||||||
<Link
|
<Link
|
||||||
id="reset-password-button"
|
id="reset-password-button"
|
||||||
component="button"
|
component="button"
|
||||||
|
@ -171,6 +173,8 @@ const useStyles = makeStyles(theme => ({
|
||||||
},
|
},
|
||||||
resetLink: {
|
resetLink: {
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
paddingTop: 13.5,
|
||||||
|
paddingBottom: 13.5,
|
||||||
},
|
},
|
||||||
rememberMe: {
|
rememberMe: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
|
|
@ -16,7 +16,11 @@ import { SecondFactorMethod } from "../../models/Methods";
|
||||||
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
||||||
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
||||||
|
|
||||||
export default function () {
|
export interface Props {
|
||||||
|
rememberMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (props: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
|
@ -114,6 +118,7 @@ export default function () {
|
||||||
<ComponentOrLoading ready={firstFactorReady}>
|
<ComponentOrLoading ready={firstFactorReady}>
|
||||||
<FirstFactorForm
|
<FirstFactorForm
|
||||||
disabled={firstFactorDisabled}
|
disabled={firstFactorDisabled}
|
||||||
|
rememberMe={props.rememberMe}
|
||||||
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
||||||
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
||||||
onAuthenticationSuccess={handleAuthSuccess} />
|
onAuthenticationSuccess={handleAuthSuccess} />
|
||||||
|
|
Loading…
Reference in New Issue