mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
104a61ecd6
Prevents the TOTP user config from being requested when the user has not registered or is already authenticated 2FA.
308 lines
10 KiB
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
|
|
}
|