2019-04-24 21:52:08 +00:00
package handlers
import (
"fmt"
2020-05-20 22:03:15 +00:00
"math"
"math/rand"
"sync"
2019-04-24 21:52:08 +00:00
"time"
2021-08-11 01:04:35 +00:00
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
2019-04-24 21:52:08 +00:00
)
2020-05-20 22:03:15 +00:00
func movingAverageIteration ( value time . Duration , successful bool , movingAverageCursor * int , execDurationMovingAverage * [ ] time . Duration , mutex sync . Locker ) float64 {
mutex . Lock ( )
if successful {
( * execDurationMovingAverage ) [ * movingAverageCursor ] = value
2021-07-22 03:52:37 +00:00
* movingAverageCursor = ( * movingAverageCursor + 1 ) % loginDelayMovingAverageWindow
2020-05-20 22:03:15 +00:00
}
var sum int64
for _ , v := range * execDurationMovingAverage {
sum += v . Milliseconds ( )
}
mutex . Unlock ( )
2021-07-22 03:52:37 +00:00
return float64 ( sum / loginDelayMovingAverageWindow )
2020-05-20 22:03:15 +00:00
}
func calculateActualDelay ( ctx * middlewares . AutheliaCtx , execDuration time . Duration , avgExecDurationMs float64 , successful * bool ) float64 {
2021-07-22 03:52:37 +00:00
randomDelayMs := float64 ( rand . Int63n ( loginDelayMaximumRandomDelayMilliseconds ) ) //nolint:gosec // TODO: Consider use of crypto/rand, this should be benchmarked and measured first.
totalDelayMs := math . Max ( avgExecDurationMs , loginDelayMinimumDelayMilliseconds ) + randomDelayMs
2020-05-20 22:03:15 +00:00
actualDelayMs := math . Max ( totalDelayMs - float64 ( execDuration . Milliseconds ( ) ) , 1.0 )
2021-09-17 05:53:40 +00:00
ctx . Logger . Tracef ( "Attempt successful: %t, exec duration: %d, avg execution duration: %d, random delay ms: %d, total delay ms: %d, actual delay ms: %d" , * successful , execDuration . Milliseconds ( ) , int64 ( avgExecDurationMs ) , int64 ( randomDelayMs ) , int64 ( totalDelayMs ) , int64 ( actualDelayMs ) )
2020-05-20 22:03:15 +00:00
return actualDelayMs
}
func delayToPreventTimingAttacks ( ctx * middlewares . AutheliaCtx , requestTime time . Time , successful * bool , movingAverageCursor * int , execDurationMovingAverage * [ ] time . Duration , mutex sync . Locker ) {
execDuration := time . Since ( requestTime )
avgExecDurationMs := movingAverageIteration ( execDuration , * successful , movingAverageCursor , execDurationMovingAverage , mutex )
actualDelayMs := calculateActualDelay ( ctx , execDuration , avgExecDurationMs , successful )
time . Sleep ( time . Duration ( actualDelayMs ) * time . Millisecond )
}
2019-04-24 21:52:08 +00:00
// FirstFactorPost is the handler performing the first factory.
2020-05-04 19:39:25 +00:00
//nolint:gocyclo // TODO: Consider refactoring time permitting.
2020-05-20 22:03:15 +00:00
func FirstFactorPost ( msInitialDelay time . Duration , delayEnabled bool ) middlewares . RequestHandler {
2021-07-22 03:52:37 +00:00
var execDurationMovingAverage = make ( [ ] time . Duration , loginDelayMovingAverageWindow )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
var movingAverageCursor = 0
var mutex = & sync . Mutex { }
for i := range execDurationMovingAverage {
execDurationMovingAverage [ i ] = msInitialDelay * time . Millisecond
2019-04-24 21:52:08 +00:00
}
2020-05-20 22:03:15 +00:00
rand . Seed ( time . Now ( ) . UnixNano ( ) )
return func ( ctx * middlewares . AutheliaCtx ) {
var successful bool
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
requestTime := time . Now ( )
if delayEnabled {
defer delayToPreventTimingAttacks ( ctx , requestTime , & successful , & movingAverageCursor , & execDurationMovingAverage , mutex )
}
bodyJSON := firstFactorRequestBody { }
err := ctx . ParseBody ( & bodyJSON )
if err != nil {
2021-07-22 03:52:37 +00:00
handleAuthenticationUnauthorized ( ctx , err , messageAuthenticationFailed )
2019-11-30 16:49:52 +00:00
return
}
2020-05-05 19:35:32 +00:00
2021-11-23 09:45:38 +00:00
bannedUntil , err := ctx . Providers . Regulator . Regulate ( ctx , bodyJSON . Username )
2020-05-05 19:35:32 +00:00
2020-05-20 22:03:15 +00:00
if err != nil {
if err == regulation . ErrUserIsBanned {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "user %s is banned until %s" , bodyJSON . Username , bannedUntil ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
2019-04-24 21:52:08 +00:00
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to regulate authentication: %s" , err . Error ( ) ) , messageAuthenticationFailed )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
return
}
2019-11-24 20:27:59 +00:00
2020-05-20 22:03:15 +00:00
userPasswordOk , err := ctx . Providers . UserProvider . CheckUserPassword ( bodyJSON . Username , bodyJSON . Password )
2020-05-05 19:35:32 +00:00
2020-05-20 22:03:15 +00:00
if err != nil {
ctx . Logger . Debugf ( "Mark authentication attempt made by user %s" , bodyJSON . Username )
2020-12-16 01:47:31 +00:00
2021-11-23 09:45:38 +00:00
if err := ctx . Providers . Regulator . Mark ( ctx , bodyJSON . Username , false ) ; err != nil {
2020-12-16 01:47:31 +00:00
ctx . Logger . Errorf ( "Unable to mark authentication: %s" , err . Error ( ) )
}
2019-04-24 21:52:08 +00:00
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "error while checking password for user %s: %s" , bodyJSON . Username , err . Error ( ) ) , messageAuthenticationFailed )
2019-11-30 16:49:52 +00:00
2020-05-20 22:03:15 +00:00
return
}
2020-05-05 21:27:38 +00:00
2020-05-20 22:03:15 +00:00
if ! userPasswordOk {
ctx . Logger . Debugf ( "Mark authentication attempt made by user %s" , bodyJSON . Username )
2020-05-05 19:35:32 +00:00
2021-11-23 09:45:38 +00:00
if err := ctx . Providers . Regulator . Mark ( ctx , bodyJSON . Username , false ) ; err != nil {
2020-12-16 01:47:31 +00:00
ctx . Logger . Errorf ( "Unable to mark authentication: %s" , err . Error ( ) )
}
2019-04-24 21:52:08 +00:00
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "credentials are wrong for user %s" , bodyJSON . Username ) , messageAuthenticationFailed )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
return
}
2019-11-30 14:33:45 +00:00
2020-05-20 22:03:15 +00:00
ctx . Logger . Debugf ( "Mark authentication attempt made by user %s" , bodyJSON . Username )
2021-11-23 09:45:38 +00:00
err = ctx . Providers . Regulator . Mark ( ctx , bodyJSON . Username , true )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
if err != nil {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to mark authentication: %s" , err . Error ( ) ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
2019-04-24 21:52:08 +00:00
2020-12-16 01:47:31 +00:00
ctx . Logger . Debugf ( "Credentials validation of user %s is ok" , bodyJSON . Username )
2021-05-04 22:06:05 +00:00
userSession := ctx . GetSession ( )
newSession := session . NewDefaultUserSession ( )
newSession . OIDCWorkflowSession = userSession . OIDCWorkflowSession
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
err = ctx . SaveSession ( newSession )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
if err != nil {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to reset the session for user %s: %s" , bodyJSON . Username , err . Error ( ) ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
err = ctx . Providers . SessionProvider . RegenerateSession ( ctx . RequestCtx )
2020-04-03 23:11:33 +00:00
2019-04-24 21:52:08 +00:00
if err != nil {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to regenerate session for user %s: %s" , bodyJSON . Username , err . Error ( ) ) , messageAuthenticationFailed )
2019-04-24 21:52:08 +00:00
return
}
2020-05-20 22:03:15 +00:00
// 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
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
// 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 {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to update expiration timer for user %s: %s" , bodyJSON . Username , err . Error ( ) ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
}
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
// Get the details of the given user from the user provider.
userDetails , err := ctx . Providers . UserProvider . GetDetails ( bodyJSON . Username )
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
if err != nil {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "error while retrieving details from user %s: %s" , bodyJSON . Username , err . Error ( ) ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
2020-05-05 19:35:32 +00:00
2020-05-20 22:03:15 +00:00
ctx . Logger . Tracef ( "Details for user %s => groups: %s, emails %s" , bodyJSON . Username , userDetails . Groups , userDetails . Emails )
2020-05-05 19:35:32 +00:00
feat(oidc): add additional config options, accurate token times, and refactoring (#1991)
* This gives admins more control over their OIDC installation exposing options that had defaults before. Things like lifespans for authorize codes, access tokens, id tokens, refresh tokens, a option to enable the debug client messages, minimum parameter entropy. It also allows admins to configure the response modes.
* Additionally this records specific values about a users session indicating when they performed a specific authz factor so this is represented in the token accurately.
* Lastly we also implemented a OIDC key manager which calculates the kid for jwk's using the SHA1 digest instead of being static, or more specifically the first 7 chars. As per https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key#section-8.1.1 the kid should not exceed 8 chars. While it's allowed to exceed 8 chars, it must only be done so with a compelling reason, which we do not have.
2021-07-03 23:44:30 +00:00
userSession . SetOneFactor ( ctx . Clock . Now ( ) , userDetails , keepMeLoggedIn )
if refresh , refreshInterval := getProfileRefreshSettings ( ctx . Configuration . AuthenticationBackend ) ; refresh {
2020-05-20 22:03:15 +00:00
userSession . RefreshTTL = ctx . Clock . Now ( ) . Add ( refreshInterval )
}
2019-04-24 21:52:08 +00:00
2020-05-20 22:03:15 +00:00
err = ctx . SaveSession ( userSession )
if err != nil {
2021-09-17 05:53:40 +00:00
handleAuthenticationUnauthorized ( ctx , fmt . Errorf ( "unable to save session of user %s" , bodyJSON . Username ) , messageAuthenticationFailed )
2020-05-20 22:03:15 +00:00
return
}
successful = true
2021-05-04 22:06:05 +00:00
if userSession . OIDCWorkflowSession != nil {
feat(oidc): add additional config options, accurate token times, and refactoring (#1991)
* This gives admins more control over their OIDC installation exposing options that had defaults before. Things like lifespans for authorize codes, access tokens, id tokens, refresh tokens, a option to enable the debug client messages, minimum parameter entropy. It also allows admins to configure the response modes.
* Additionally this records specific values about a users session indicating when they performed a specific authz factor so this is represented in the token accurately.
* Lastly we also implemented a OIDC key manager which calculates the kid for jwk's using the SHA1 digest instead of being static, or more specifically the first 7 chars. As per https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key#section-8.1.1 the kid should not exceed 8 chars. While it's allowed to exceed 8 chars, it must only be done so with a compelling reason, which we do not have.
2021-07-03 23:44:30 +00:00
handleOIDCWorkflowResponse ( ctx )
2021-05-04 22:06:05 +00:00
} else {
Handle1FAResponse ( ctx , bodyJSON . TargetURL , bodyJSON . RequestMethod , userSession . Username , userSession . Groups )
}
2020-05-20 22:03:15 +00:00
}
2019-04-24 21:52:08 +00:00
}