authelia/internal/handlers/handler_sign_duo.go

308 lines
10 KiB
Go

package handlers
import (
"fmt"
"net/url"
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// SecondFactorDuoPost handler for sending a push notification via duo api.
func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
var (
requestBody signDuoRequestBody
device, method string
)
if err := ctx.ParseBody(&requestBody); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession := ctx.GetSession()
remoteIP := ctx.RemoteIP().String()
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)
}
if err != nil {
ctx.Error(err, messageMFAValidationFailed)
return
}
if device == "" || method == "" {
return
}
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)
if err != nil {
ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
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
}
if authResponse.Result != allow {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeDuo,
fmt.Errorf("duo auth result: %s, status: %s, message: %s", authResponse.Result, authResponse.Status,
authResponse.StatusMessage))
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeDuo, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
HandleAllow(ctx, requestBody.TargetURL)
}
}
// 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)
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")
}
return "", "", nil
case deny:
ctx.Logger.Infof("Duo user: %s not allowed to authenticate: %s", userSession.Username, message)
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)
if err != nil {
return "", "", err
}
return device, method, nil
}
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")
}
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)
} else {
return nil, fmt.Errorf("no passcode received from user: %s", userSession.Username)
}
}
return values, nil
}