Disable inactivity timeout when user checked remember me.

Instead of checking the value of the cookie expiration we rely
on the boolean stored in the user session to check whether inactivity
timeout should be disabled.
pull/554/head
Clement Michaud 2020-01-17 23:48:48 +01:00 committed by Clément Michaud
parent 6792fd5bc3
commit 841de2b75d
8 changed files with 174 additions and 32 deletions

View File

@ -102,6 +102,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
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)
if err != nil { if err != nil {

View File

@ -151,7 +151,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.") s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
} }
func (s *FirstFactorSuite) TestShouldAuthenticateUser() { func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
s.mock.UserProviderMock. s.mock.UserProviderMock.
EXPECT(). EXPECT().
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
@ -184,10 +184,49 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUser() {
// And store authentication in session. // And store authentication in session.
session := s.mock.Ctx.GetSession() session := s.mock.Ctx.GetSession()
assert.Equal(s.T(), "test", session.Username) assert.Equal(s.T(), "test", session.Username)
assert.Equal(s.T(), true, session.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel) assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails) assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups) assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
}
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
s.mock.UserProviderMock.
EXPECT().
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
Return(true, nil)
s.mock.UserProviderMock.
EXPECT().
GetDetails(gomock.Eq("test")).
Return(&authentication.UserDetails{
Emails: []string{"test@example.com"},
Groups: []string{"dev", "admins"},
}, nil)
s.mock.StorageProviderMock.
EXPECT().
AppendAuthenticationLog(gomock.Any()).
Return(nil)
s.mock.Ctx.Request.SetBodyString(`{
"username": "test",
"password": "hello",
"keepMeLoggedIn": false
}`)
FirstFactorPost(s.mock.Ctx)
// Respond with 200.
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
// And store authentication in session.
session := s.mock.Ctx.GetSession()
assert.Equal(s.T(), "test", session.Username)
assert.Equal(s.T(), false, session.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups)
} }
func TestFirstFactorSuite(t *testing.T) { func TestFirstFactorSuite(t *testing.T) {

View File

@ -139,24 +139,13 @@ 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) {
expiration, err := ctx.Providers.SessionProvider.GetExpiration(ctx.RequestCtx)
if err != nil {
return false, err
}
// If the cookie has no expiration.
if expiration == 0 {
return false, nil
}
maxInactivityPeriod := ctx.Configuration.Session.Inactivity maxInactivityPeriod := ctx.Configuration.Session.Inactivity
if maxInactivityPeriod == 0 { if maxInactivityPeriod == 0 {
return false, nil return false, nil
} }
lastActivity := ctx.GetSession().LastActivity lastActivity := ctx.GetSession().LastActivity
inactivityPeriod := time.Now().Unix() - lastActivity inactivityPeriod := ctx.Clock.Now().Unix() - lastActivity
ctx.Logger.Tracef("Inactivity report: Inactivity=%d, MaxInactivity=%d", ctx.Logger.Tracef("Inactivity report: Inactivity=%d, MaxInactivity=%d",
inactivityPeriod, maxInactivityPeriod) inactivityPeriod, maxInactivityPeriod)
@ -178,7 +167,7 @@ func verifyFromSessionCookie(targetURL url.URL, ctx *middlewares.AutheliaCtx) (u
return "", nil, authentication.NotAuthenticated, fmt.Errorf("An anonymous user cannot be authenticated. That might be the sign of a compromise") return "", nil, authentication.NotAuthenticated, fmt.Errorf("An anonymous user cannot be authenticated. That might be the sign of a compromise")
} }
if !isUserAnonymous { if !userSession.KeepMeLoggedIn && !isUserAnonymous {
inactiveLongEnough, err := hasUserBeenInactiveLongEnough(ctx) inactiveLongEnough, err := hasUserBeenInactiveLongEnough(ctx)
if err != nil { if err != nil {
return "", nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check if user has been inactive for a long time: %s", err) return "", nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check if user has been inactive for a long time: %s", err)

View File

@ -5,6 +5,7 @@ import (
"net" "net"
"net/url" "net/url"
"testing" "testing"
"time"
"github.com/authelia/authelia/internal/authentication" "github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/authorization" "github.com/authelia/authelia/internal/authorization"
@ -426,3 +427,79 @@ func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
}) })
} }
} }
func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
clock := mocks.TestingClock{}
clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
userSession.Username = "john"
userSession.AuthenticationLevel = authentication.TwoFactor
userSession.LastActivity = clock.Now().Add(-1 * time.Hour).Unix()
mock.Ctx.SaveSession(userSession)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
VerifyGet(mock.Ctx)
// The session has been destroyed
newUserSession := mock.Ctx.GetSession()
assert.Equal(t, "", newUserSession.Username)
assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
}
func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
clock := mocks.TestingClock{}
clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
userSession.Username = "john"
userSession.AuthenticationLevel = authentication.TwoFactor
userSession.LastActivity = clock.Now().Add(-1 * time.Hour).Unix()
userSession.KeepMeLoggedIn = true
mock.Ctx.SaveSession(userSession)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
VerifyGet(mock.Ctx)
// The session has been destroyed
newUserSession := mock.Ctx.GetSession()
assert.Equal(t, "john", newUserSession.Username)
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
}
func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
clock := mocks.TestingClock{}
clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = 10
userSession := mock.Ctx.GetSession()
userSession.Username = "john"
userSession.AuthenticationLevel = authentication.TwoFactor
userSession.LastActivity = clock.Now().Add(-1 * time.Second).Unix()
mock.Ctx.SaveSession(userSession)
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
VerifyGet(mock.Ctx)
// The session has been destroyed
newUserSession := mock.Ctx.GetSession()
assert.Equal(t, "john", newUserSession.Username)
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/session" "github.com/authelia/authelia/internal/session"
"github.com/authelia/authelia/internal/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@ -29,13 +30,7 @@ func NewAutheliaCtx(ctx *fasthttp.RequestCtx, configuration schema.Configuration
autheliaCtx.Providers = providers autheliaCtx.Providers = providers
autheliaCtx.Configuration = configuration autheliaCtx.Configuration = configuration
autheliaCtx.Logger = NewRequestLogger(autheliaCtx) autheliaCtx.Logger = NewRequestLogger(autheliaCtx)
autheliaCtx.Clock = utils.RealClock{}
userSession, err := providers.SessionProvider.GetSession(ctx)
if err != nil {
return autheliaCtx, fmt.Errorf("Unable to retrieve user session: %s", err.Error())
}
autheliaCtx.userSession = userSession
return autheliaCtx, nil return autheliaCtx, nil
} }
@ -112,12 +107,16 @@ func (c *AutheliaCtx) XOriginalURL() []byte {
// GetSession return the user session. Any update will be saved in cache. // GetSession return the user session. Any update will be saved in cache.
func (c *AutheliaCtx) GetSession() session.UserSession { func (c *AutheliaCtx) GetSession() session.UserSession {
return c.userSession userSession, err := c.Providers.SessionProvider.GetSession(c.RequestCtx)
if err != nil {
c.Logger.Error("Unable to retrieve user session")
return session.NewDefaultUserSession()
}
return userSession
} }
// SaveSession save the content of the session. // SaveSession save the content of the session.
func (c *AutheliaCtx) SaveSession(userSession session.UserSession) error { func (c *AutheliaCtx) SaveSession(userSession session.UserSession) error {
c.userSession = userSession
return c.Providers.SessionProvider.SaveSession(c.RequestCtx, userSession) return c.Providers.SessionProvider.SaveSession(c.RequestCtx, userSession)
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/authelia/authelia/internal/regulation" "github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/session" "github.com/authelia/authelia/internal/session"
"github.com/authelia/authelia/internal/storage" "github.com/authelia/authelia/internal/storage"
"github.com/authelia/authelia/internal/utils"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -20,7 +21,8 @@ type AutheliaCtx struct {
Logger *logrus.Entry Logger *logrus.Entry
Providers Providers Providers Providers
Configuration schema.Configuration Configuration schema.Configuration
userSession session.UserSession
Clock utils.Clock
} }
// Providers contain all provider provided to Authelia. // Providers contain all provider provided to Authelia.

View File

@ -6,6 +6,7 @@ import (
"github.com/authelia/authelia/internal/authentication" "github.com/authelia/authelia/internal/authentication"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -20,7 +21,8 @@ func TestShouldInitializerSession(t *testing.T) {
configuration.Expiration = 40 configuration.Expiration = 40
provider := NewProvider(configuration) provider := NewProvider(configuration)
session, _ := provider.GetSession(ctx) session, err := provider.GetSession(ctx)
require.NoError(t, err)
assert.Equal(t, NewDefaultUserSession(), session) assert.Equal(t, NewDefaultUserSession(), session)
} }
@ -38,12 +40,45 @@ func TestShouldUpdateSession(t *testing.T) {
session.Username = "john" session.Username = "john"
session.AuthenticationLevel = authentication.TwoFactor session.AuthenticationLevel = authentication.TwoFactor
_ = provider.SaveSession(ctx, session) err := provider.SaveSession(ctx, session)
require.NoError(t, err)
session, _ = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
require.NoError(t, err)
assert.Equal(t, UserSession{ assert.Equal(t, UserSession{
Username: "john", Username: "john",
AuthenticationLevel: authentication.TwoFactor, AuthenticationLevel: authentication.TwoFactor,
}, session) }, session)
} }
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{}
configuration.Domain = "example.com"
configuration.Name = "my_session"
configuration.Expiration = 40
provider := NewProvider(configuration)
session, err := provider.GetSession(ctx)
require.NoError(t, err)
session.Username = "john"
session.AuthenticationLevel = authentication.TwoFactor
err = provider.SaveSession(ctx, session)
require.NoError(t, err)
newUserSession, err := provider.GetSession(ctx)
require.NoError(t, err)
assert.Equal(t, "john", newUserSession.Username)
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
err = provider.DestroySession(ctx)
require.NoError(t, err)
newUserSession, err = provider.GetSession(ctx)
require.NoError(t, err)
assert.Equal(t, "", newUserSession.Username)
assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel)
}

View File

@ -106,7 +106,7 @@ func (s *InactivityScenario) TestShouldDisableCookieExpirationAndInactivity() {
s.doVisit(s.T(), HomeBaseURL) s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T()) s.verifyIsHome(ctx, s.T())
time.Sleep(9 * time.Second) time.Sleep(10 * time.Second)
s.doVisit(s.T(), targetURL) s.doVisit(s.T(), targetURL)
s.verifySecretAuthorized(ctx, s.T()) s.verifySecretAuthorized(ctx, s.T())