authelia/internal/handlers/handler_firstfactor.go
James Elliott 3f374534ab
[FEATURE] Automatic Profile Refresh - LDAP (#912)
* [FIX] LDAP Not Checking for Updated Groups

* refactor handlers verifyFromSessionCookie
* refactor authorizer selectMatchingObjectRules
* refactor authorizer isDomainMatching
* add authorizer URLHasGroupSubjects method
* add user provider ProviderType method
* update tests
* check for new LDAP groups and update session when:
  * user provider type is LDAP
  * authorization is forbidden
  * URL has rule with group subjects

* Implement Refresh Interval

* add default values for LDAP user provider
* add default for refresh interval
* add schema validator for refresh interval
* add various tests
* rename hasUserBeenInactiveLongEnough to hasUserBeenInactiveTooLong
* use Authelia ctx clock
* add check to determine if user is deleted, if so destroy the
* make ldap user not found error a const
* implement GetRefreshSettings in mock

* Use user not found const with FileProvider
* comment exports

* use ctx.Clock instead of time pkg

* add debug logging

* use ptr to reference userSession so we don't have to retrieve it again

* add documenation
* add check for 0 refresh interval to reduce CPU cost
* remove badly copied debug msg

* add group change delta message

* add SliceStringDelta
* refactor ldap refresh to use the new func

* improve delta add/remove log message

* fix incorrect logic in SliceStringDelta
* add tests to SliceStringDelta

* add always config option
* add tests for always config option
* update docs

* apply suggestions from code review

Co-Authored-By: Amir Zarrinkafsh <nightah@me.com>

* complete mocks and fix an old one
* show warning when LDAP details failed to update for an unknown reason

* golint fix

* actually fix existing mocks

* use mocks for LDAP refresh testing

* use mocks for LDAP refresh testing for both added and removed groups

* use test mock to verify disabled refresh behaviour
* add information to threat model
* add time const for default Unix() value

* misc adjustments to mocks

* Suggestions from code review

* requested changes
* update emails
* docs updates
* test updates
* misc

* golint fix

* set debug for dev testing

* misc docs and logging updates

* misc grammar/spelling

* use built function for VerifyGet

* fix reviewdog suggestions

* requested changes

* Apply suggestions from code review

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
Co-authored-by: Clément Michaud <clement.michaud34@gmail.com>
2020-05-04 21:39:25 +02:00

121 lines
4.4 KiB
Go

package handlers
import (
"fmt"
"time"
"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/middlewares"
"github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/session"
)
// FirstFactorPost is the handler performing the first factory.
//nolint:gocyclo // TODO: Consider refactoring time permitting.
func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
bodyJSON := firstFactorRequestBody{}
err := ctx.ParseBody(&bodyJSON)
if err != nil {
ctx.Error(err, authenticationFailedMessage)
return
}
bannedUntil, err := ctx.Providers.Regulator.Regulate(bodyJSON.Username)
if err != nil {
if err == regulation.ErrUserIsBanned {
ctx.Error(fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage)
return
}
ctx.Error(fmt.Errorf("Unable to regulate authentication: %s", err), authenticationFailedMessage)
return
}
userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password)
if err != nil {
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
return
}
if !userPasswordOk {
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
ctx.Providers.Regulator.Mark(bodyJSON.Username, false) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
ctx.ReplyError(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage)
return
}
ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username)
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, true)
if err != nil {
ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage)
return
}
// Reset all values from previous session before regenerating the cookie.
err = ctx.SaveSession(session.NewDefaultUserSession())
if err != nil {
ctx.Error(fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
return
}
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil {
ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
return
}
// 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
}
}
// Get the details of the given user from the user provider.
userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username)
if err != nil {
ctx.Error(fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
return
}
ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails)
// And set those information in the new session.
userSession := ctx.GetSession()
userSession.Username = userDetails.Username
userSession.Groups = userDetails.Groups
userSession.Emails = userDetails.Emails
userSession.AuthenticationLevel = authentication.OneFactor
userSession.LastActivity = time.Now().Unix()
userSession.KeepMeLoggedIn = keepMeLoggedIn
refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend)
if refresh {
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval)
}
err = ctx.SaveSession(userSession)
if err != nil {
ctx.Error(fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage)
return
}
Handle1FAResponse(ctx, bodyJSON.TargetURL, userSession.Username, userSession.Groups)
}