2019-04-25 04:52:08 +07:00
package handlers
import (
"fmt"
"net/url"
2021-08-11 08:04:35 +07:00
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/middlewares"
2021-12-01 10:32:58 +07:00
"github.com/authelia/authelia/v4/internal/models"
2021-11-29 10:09:14 +07:00
"github.com/authelia/authelia/v4/internal/regulation"
2021-12-01 10:32:58 +07:00
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
2019-04-25 04:52:08 +07:00
)
// SecondFactorDuoPost handler for sending a push notification via duo api.
func SecondFactorDuoPost ( duoAPI duo . API ) middlewares . RequestHandler {
return func ( ctx * middlewares . AutheliaCtx ) {
2021-12-01 10:32:58 +07:00
var (
requestBody signDuoRequestBody
device , method string
)
2019-04-25 04:52:08 +07:00
2021-11-29 10:09:14 +07:00
if err := ctx . ParseBody ( & requestBody ) ; err != nil {
ctx . Logger . Errorf ( logFmtErrParseRequestBody , regulation . AuthTypeDUO , err )
respondUnauthorized ( ctx , messageMFAValidationFailed )
2019-04-25 04:52:08 +07:00
return
}
userSession := ctx . GetSession ( )
2020-03-01 07:51:11 +07:00
remoteIP := ctx . RemoteIP ( ) . String ( )
2021-12-01 10:32:58 +07:00
duoDevice , err := ctx . Providers . StorageProvider . LoadPreferredDuoDevice ( ctx , userSession . Username )
if err != nil {
ctx . Logger . Debugf ( "Error identifying preferred device for user %s: %s" , userSession . Username , err )
ctx . Logger . Debugf ( "Starting Duo PreAuth for initial device selection of user: %s" , userSession . Username )
device , method , err = HandleInitialDeviceSelection ( ctx , & userSession , duoAPI , requestBody . TargetURL )
} else {
ctx . Logger . Debugf ( "Starting Duo PreAuth to check preferred device of user: %s" , userSession . Username )
device , method , err = HandlePreferredDeviceCheck ( ctx , & userSession , duoAPI , duoDevice . Device , duoDevice . Method , requestBody . TargetURL )
}
2021-11-29 10:09:14 +07:00
2021-12-01 10:32:58 +07:00
if err != nil {
ctx . Error ( err , messageMFAValidationFailed )
return
}
2020-05-06 02:35:32 +07:00
2021-12-01 10:32:58 +07:00
if device == "" || method == "" {
return
2019-04-25 04:52:08 +07:00
}
2021-12-01 10:32:58 +07:00
ctx . Logger . Debugf ( "Starting Duo Auth attempt for %s with device %s and method %s from IP %s" , userSession . Username , device , method , remoteIP )
values , err := SetValues ( userSession , device , method , remoteIP , requestBody . TargetURL , requestBody . Passcode )
2019-04-25 04:52:08 +07:00
if err != nil {
2021-12-01 10:32:58 +07:00
ctx . Logger . Errorf ( "Failed to set values for Duo Auth Call for user '%s': %+v" , userSession . Username , err )
2021-11-29 10:09:14 +07:00
respondUnauthorized ( ctx , messageMFAValidationFailed )
2019-04-25 04:52:08 +07:00
return
}
2021-12-01 10:32:58 +07:00
authResponse , err := duoAPI . AuthCall ( ctx , values )
if err != nil {
ctx . Logger . Errorf ( "Failed to perform Duo Auth Call for user '%s': %+v" , userSession . Username , err )
respondUnauthorized ( ctx , messageMFAValidationFailed )
return
2020-03-01 07:51:11 +07:00
}
2021-12-01 10:32:58 +07:00
if authResponse . Result != allow {
2021-11-29 10:09:14 +07:00
_ = markAuthenticationAttempt ( ctx , false , nil , userSession . Username , regulation . AuthTypeDUO ,
2021-12-01 10:32:58 +07:00
fmt . Errorf ( "duo auth result: %s, status: %s, message: %s" , authResponse . Result , authResponse . Status ,
authResponse . StatusMessage ) )
2021-11-29 10:09:14 +07:00
respondUnauthorized ( ctx , messageMFAValidationFailed )
2019-04-25 04:52:08 +07:00
return
}
2021-11-29 10:09:14 +07:00
if err = markAuthenticationAttempt ( ctx , true , nil , userSession . Username , regulation . AuthTypeDUO , nil ) ; err != nil {
respondUnauthorized ( ctx , messageMFAValidationFailed )
return
}
2021-12-01 10:32:58 +07:00
HandleAllow ( ctx , requestBody . TargetURL )
}
}
2021-11-29 10:09:14 +07:00
2021-12-01 10:32:58 +07:00
// HandleInitialDeviceSelection handler for retrieving all available devices.
func HandleInitialDeviceSelection ( ctx * middlewares . AutheliaCtx , userSession * session . UserSession , duoAPI duo . API , targetURL string ) ( device string , method string , err error ) {
result , message , devices , enrollURL , err := DuoPreAuth ( ctx , duoAPI )
if err != nil {
ctx . Logger . Errorf ( "Failed to perform Duo PreAuth for user '%s': %+v" , userSession . Username , err )
2020-03-01 06:13:33 +07:00
2021-12-01 10:32:58 +07:00
respondUnauthorized ( ctx , messageMFAValidationFailed )
return "" , "" , err
}
switch result {
case enroll :
ctx . Logger . Debugf ( "Duo user: %s not enrolled" , userSession . Username )
if err := ctx . SetJSONBody ( DuoSignResponse { Result : enroll , EnrollURL : enrollURL } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
2020-03-01 06:13:33 +07:00
}
2021-12-01 10:32:58 +07:00
return "" , "" , nil
case deny :
ctx . Logger . Infof ( "Duo user: %s not allowed to authenticate: %s" , userSession . Username , message )
2019-04-25 04:52:08 +07:00
2021-12-01 10:32:58 +07:00
if err := ctx . SetJSONBody ( DuoSignResponse { Result : deny } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
}
return "" , "" , nil
case allow :
ctx . Logger . Debugf ( "Duo authentication was bypassed for user: %s" , userSession . Username )
HandleAllow ( ctx , targetURL )
return "" , "" , nil
case auth :
device , method , err = HandleAutoSelection ( ctx , devices , userSession . Username )
2019-04-25 04:52:08 +07:00
if err != nil {
2021-12-01 10:32:58 +07:00
return "" , "" , err
}
2021-11-29 10:09:14 +07:00
2021-12-01 10:32:58 +07:00
return device , method , nil
}
2021-11-29 10:09:14 +07:00
2021-12-01 10:32:58 +07:00
return "" , "" , fmt . Errorf ( "unknown result: %s" , result )
}
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
func HandlePreferredDeviceCheck ( ctx * middlewares . AutheliaCtx , userSession * session . UserSession , duoAPI duo . API , device string , method string , targetURL string ) ( string , string , error ) {
result , message , devices , enrollURL , err := DuoPreAuth ( ctx , duoAPI )
if err != nil {
ctx . Logger . Errorf ( "Failed to perform Duo PreAuth for user '%s': %+v" , userSession . Username , err )
respondUnauthorized ( ctx , messageMFAValidationFailed )
return "" , "" , nil
}
switch result {
case enroll :
ctx . Logger . Debugf ( "Duo user: %s no longer enrolled removing preferred device" , userSession . Username )
if err := ctx . Providers . StorageProvider . DeletePreferredDuoDevice ( ctx , userSession . Username ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to delete preferred Duo device and method for user %s: %s" , userSession . Username , err )
}
if err := ctx . SetJSONBody ( DuoSignResponse { Result : enroll , EnrollURL : enrollURL } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
}
return "" , "" , nil
case deny :
ctx . Logger . Infof ( "Duo user: %s not allowed to authenticate: %s" , userSession . Username , message )
ctx . ReplyUnauthorized ( )
return "" , "" , nil
case allow :
ctx . Logger . Debugf ( "Duo authentication was bypassed for user: %s" , userSession . Username )
HandleAllow ( ctx , targetURL )
return "" , "" , nil
case auth :
if devices == nil {
ctx . Logger . Debugf ( "Duo user: %s has no compatible device/method available removing preferred device" , userSession . Username )
if err := ctx . Providers . StorageProvider . DeletePreferredDuoDevice ( ctx , userSession . Username ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to delete preferred Duo device and method for user %s: %s" , userSession . Username , err )
}
if err := ctx . SetJSONBody ( DuoSignResponse { Result : enroll } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
}
return "" , "" , nil
}
if len ( devices ) > 0 {
for i := range devices {
if devices [ i ] . Device == device {
if utils . IsStringInSlice ( method , devices [ i ] . Capabilities ) {
return device , method , nil
}
}
}
}
return HandleAutoSelection ( ctx , devices , userSession . Username )
}
return "" , "" , fmt . Errorf ( "unknown result: %s" , result )
}
// HandleAutoSelection handler automatically selects preferred device if there is only one suitable option.
func HandleAutoSelection ( ctx * middlewares . AutheliaCtx , devices [ ] DuoDevice , username string ) ( string , string , error ) {
if devices == nil {
ctx . Logger . Debugf ( "No compatible device/method available for Duo user: %s" , username )
if err := ctx . SetJSONBody ( DuoSignResponse { Result : enroll } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
}
return "" , "" , nil
}
if len ( devices ) > 1 {
ctx . Logger . Debugf ( "Multiple devices available for Duo user: %s require manual selection" , username )
if err := ctx . SetJSONBody ( DuoSignResponse { Result : auth , Devices : devices } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
}
return "" , "" , nil
}
if len ( devices [ 0 ] . Capabilities ) > 1 {
ctx . Logger . Debugf ( "Multiple methods available for Duo user: %s require manual selection" , username )
if err := ctx . SetJSONBody ( DuoSignResponse { Result : auth , Devices : devices } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to set JSON body in response" )
2019-04-25 04:52:08 +07:00
}
2021-12-01 10:32:58 +07:00
return "" , "" , nil
}
device := devices [ 0 ] . Device
method := devices [ 0 ] . Capabilities [ 0 ]
ctx . Logger . Debugf ( "Exactly one device: '%s' and method: '%s' found, saving as new preferred Duo device and method for user: %s" , device , method , username )
if err := ctx . Providers . StorageProvider . SavePreferredDuoDevice ( ctx , models . DuoDevice { Username : username , Method : method , Device : device } ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to save new preferred Duo device and method for user %s: %s" , username , err )
}
return device , method , nil
}
// HandleAllow handler for successful logins.
func HandleAllow ( ctx * middlewares . AutheliaCtx , targetURL string ) {
userSession := ctx . GetSession ( )
err := ctx . Providers . SessionProvider . RegenerateSession ( ctx . RequestCtx )
if err != nil {
ctx . Logger . Errorf ( logFmtErrSessionRegenerate , regulation . AuthTypeDUO , userSession . Username , err )
respondUnauthorized ( ctx , messageMFAValidationFailed )
return
}
userSession . SetTwoFactor ( ctx . Clock . Now ( ) )
err = ctx . SaveSession ( userSession )
if err != nil {
ctx . Logger . Errorf ( logFmtErrSessionSave , "authentication time" , regulation . AuthTypeTOTP , userSession . Username , err )
respondUnauthorized ( ctx , messageMFAValidationFailed )
return
}
if userSession . OIDCWorkflowSession != nil {
handleOIDCWorkflowResponse ( ctx )
} else {
Handle2FAResponse ( ctx , targetURL )
}
}
// SetValues sets all appropriate Values for the Auth Request.
func SetValues ( userSession session . UserSession , device string , method string , remoteIP string , targetURL string , passcode string ) ( url . Values , error ) {
values := url . Values { }
values . Set ( "username" , userSession . Username )
values . Set ( "ipaddr" , remoteIP )
values . Set ( "factor" , method )
switch method {
case duo . Push :
values . Set ( "device" , device )
if userSession . DisplayName != "" {
values . Set ( "display_username" , userSession . DisplayName )
}
if targetURL != "" {
values . Set ( "pushinfo" , fmt . Sprintf ( "target%%20url=%s" , targetURL ) )
}
case duo . Phone :
values . Set ( "device" , device )
case duo . SMS :
values . Set ( "device" , device )
case duo . OTP :
if passcode != "" {
values . Set ( "passcode" , passcode )
2021-05-05 05:06:05 +07:00
} else {
2021-12-01 10:32:58 +07:00
return nil , fmt . Errorf ( "no passcode received from user: %s" , userSession . Username )
2021-05-05 05:06:05 +07:00
}
2019-04-25 04:52:08 +07:00
}
2021-12-01 10:32:58 +07:00
return values , nil
2019-04-25 04:52:08 +07:00
}