refactor: factorize verify handler (#3662)

This factorizes a few sections of the /api/verify handler and improves both the code flow and error output of the section of code.
pull/3672/head
James Elliott 2022-07-08 12:32:43 +10:00 committed by GitHub
parent a6fb7cc4ee
commit 24f5caed97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 45 deletions

View File

@ -128,24 +128,16 @@ func setForwardedHeaders(headers *fasthttp.ResponseHeader, username, name string
}
}
// hasUserBeenInactiveTooLong checks whether the user has been inactive for too long.
func hasUserBeenInactiveTooLong(ctx *middlewares.AutheliaCtx) (bool, error) { //nolint:unparam
maxInactivityPeriod := int64(ctx.Providers.SessionProvider.Inactivity.Seconds())
if maxInactivityPeriod == 0 {
return false, nil
func isSessionInactiveTooLong(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, isUserAnonymous bool) (isInactiveTooLong bool) {
if userSession.KeepMeLoggedIn || isUserAnonymous || int64(ctx.Providers.SessionProvider.Inactivity.Seconds()) == 0 {
return false
}
lastActivity := ctx.GetSession().LastActivity
inactivityPeriod := ctx.Clock.Now().Unix() - lastActivity
isInactiveTooLong = time.Unix(userSession.LastActivity, 0).Add(ctx.Providers.SessionProvider.Inactivity).Before(ctx.Clock.Now())
ctx.Logger.Tracef("Inactivity report: Inactivity=%d, MaxInactivity=%d",
inactivityPeriod, maxInactivityPeriod)
ctx.Logger.Tracef("Inactivity report for user '%s'. Current Time: %d, Last Activity: %d, Maximum Inactivity: %d.", userSession.Username, ctx.Clock.Now().Unix(), userSession.LastActivity, int(ctx.Providers.SessionProvider.Inactivity.Seconds()))
if inactivityPeriod > maxInactivityPeriod {
return true, nil
}
return false, nil
return isInactiveTooLong
}
// verifySessionCookie verifies if a user is identified by a cookie.
@ -155,40 +147,30 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
isUserAnonymous := userSession.Username == ""
if isUserAnonymous && userSession.AuthenticationLevel != authentication.NotAuthenticated {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("an anonymous user cannot be authenticated. That might be the sign of a compromise")
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("an anonymous user cannot be authenticated (this might be the sign of a security compromise)")
}
if !userSession.KeepMeLoggedIn && !isUserAnonymous {
inactiveLongEnough, err := hasUserBeenInactiveTooLong(ctx)
if err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to check if user has been inactive for a long time: %s", err)
}
if inactiveLongEnough {
if isSessionInactiveTooLong(ctx, userSession, isUserAnonymous) {
// Destroy the session a new one will be regenerated on next request.
err := ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
if err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy user session after long inactivity: %s", err)
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
return "", "", nil, nil, authentication.NotAuthenticated, fmt.Errorf("unable to destroy session for user '%s' after the session has been inactive too long: %w", userSession.Username, err)
}
ctx.Logger.Warnf("User %s has been inactive for too long", userSession.Username)
ctx.Logger.Warnf("Session destroyed for user '%s' after exceeding configured session inactivity and not being marked as remembered", userSession.Username)
return "", "", nil, nil, authentication.NotAuthenticated, nil
}
}
err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval)
if err != nil {
if err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval); err != nil {
if err == authentication.ErrUserNotFound {
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
if err != nil {
ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %s", err)
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
ctx.Logger.Errorf("Unable to destroy user session after provider refresh didn't find the user: %v", err)
}
return userSession.Username, userSession.DisplayName, userSession.Groups, userSession.Emails, authentication.NotAuthenticated, err
}
ctx.Logger.Errorf("Error occurred while attempting to update user details from LDAP: %s", err)
ctx.Logger.Errorf("Error occurred while attempting to update user details from LDAP: %v", err)
return "", "", nil, nil, authentication.NotAuthenticated, err
}
@ -415,31 +397,32 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
if authValue != nil {
isBasicAuth = true
} else if isBasicAuth {
err = fmt.Errorf("basic auth requested via query arg, but no value provided via %s header", authHeader)
return
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("basic auth requested via query arg, but no value provided via %s header", authHeader)
}
if isBasicAuth {
username, name, groups, emails, authLevel, err = verifyBasicAuth(ctx, authHeader, authValue)
return
return isBasicAuth, username, name, groups, emails, authLevel, err
}
userSession := ctx.GetSession()
username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval)
if username, name, groups, emails, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession, refreshProfile, refreshProfileInterval); err != nil {
return isBasicAuth, username, name, groups, emails, authLevel, err
}
sessionUsername := ctx.Request.Header.PeekBytes(headerSessionUsername)
if sessionUsername != nil && !strings.EqualFold(string(sessionUsername), username) {
ctx.Logger.Warnf("Possible cookie hijack or attempt to bypass security detected destroying the session and sending 401 response")
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
if err != nil {
if err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx); err != nil {
ctx.Logger.Errorf("Unable to destroy user session after handler could not match them to their %s header: %s", headerSessionUsername, err)
}
err = fmt.Errorf("could not match user %s to their %s header with a value of %s when visiting %s", username, headerSessionUsername, sessionUsername, targetURL.String())
return isBasicAuth, username, name, groups, emails, authLevel, fmt.Errorf("could not match user %s to their %s header with a value of %s when visiting %s", username, headerSessionUsername, sessionUsername, targetURL.String())
}
return
return isBasicAuth, username, name, groups, emails, authLevel, err
}
// VerifyGET returns the handler verifying if a request is allowed to go through.
@ -479,7 +462,7 @@ func VerifyGET(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
if err != nil {
ctx.Logger.Errorf("Error caught when verifying user authorization: %s", err)
if err := updateActivityTimestamp(ctx, isBasicAuth); err != nil {
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
return
}
@ -502,7 +485,7 @@ func VerifyGET(cfg schema.AuthenticationBackendConfiguration) middlewares.Reques
setForwardedHeaders(&ctx.Response.Header, username, name, groups, emails)
}
if err := updateActivityTimestamp(ctx, isBasicAuth); err != nil {
if err = updateActivityTimestamp(ctx, isBasicAuth); err != nil {
ctx.Error(fmt.Errorf("unable to update last activity: %s", err), messageOperationFailed)
}
}

View File

@ -1314,3 +1314,67 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
assert.Nil(t, mock.Ctx.Response.Header.Peek("Location"))
}
func TestIsSessionInactiveTooLong(t *testing.T) {
testCases := []struct {
name string
have *session.UserSession
now time.Time
inactivity time.Duration
expected bool
}{
{
name: "ShouldNotBeInactiveTooLong",
have: &session.UserSession{Username: "john", LastActivity: 1656994960},
now: time.Unix(1656994970, 0),
inactivity: time.Second * 90,
expected: false,
},
{
name: "ShouldNotBeInactiveTooLongIfAnonymous",
have: &session.UserSession{Username: "", LastActivity: 1656994960},
now: time.Unix(1656994990, 0),
inactivity: time.Second * 20,
expected: false,
},
{
name: "ShouldNotBeInactiveTooLongIfRemembered",
have: &session.UserSession{Username: "john", LastActivity: 1656994960, KeepMeLoggedIn: true},
now: time.Unix(1656994990, 0),
inactivity: time.Second * 20,
expected: false,
},
{
name: "ShouldNotBeInactiveTooLongIfDisabled",
have: &session.UserSession{Username: "john", LastActivity: 1656994960},
now: time.Unix(1656994990, 0),
inactivity: time.Second * 0,
expected: false,
},
{
name: "ShouldBeInactiveTooLong",
have: &session.UserSession{Username: "john", LastActivity: 1656994960},
now: time.Unix(4656994990, 0),
inactivity: time.Second * 1,
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
defer ctx.Close()
ctx.Ctx.Configuration.Session.Inactivity = tc.inactivity
ctx.Ctx.Providers.SessionProvider = session.NewProvider(ctx.Ctx.Configuration.Session, nil)
ctx.Clock.Set(tc.now)
ctx.Ctx.Clock = &ctx.Clock
actual := isSessionInactiveTooLong(ctx.Ctx, tc.have, tc.have.Username == "")
assert.Equal(t, tc.expected, actual)
})
}
}