authelia/internal/handlers/handler_sign_totp.go

219 lines
7.6 KiB
Go

package handlers
import (
"errors"
"fmt"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
)
// TimeBasedOneTimePasswordGET returns the users TOTP configuration.
func TimeBasedOneTimePasswordGET(ctx *middlewares.AutheliaCtx) {
var (
userSession session.UserSession
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving TOTP configuration: %s", errStrUserSessionData)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if userSession.IsAnonymous() {
ctx.Logger.WithError(errUserAnonymous).Error("Error occurred retrieving TOTP configuration")
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
var config *model.TOTPConfiguration
if config, err = ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving TOTP configuration for user '%s': error occurred retrieving the configuration from the storage backend", userSession.Username)
if errors.Is(err, storage.ErrNoTOTPConfiguration) {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetJSONError("Could not find TOTP Configuration for user.")
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetJSONError("Could not find TOTP Configuration for user.")
}
return
}
if err = ctx.SetJSONBody(config); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving TOTP configuration for user '%s': %s", userSession.Username, errStrRespBody)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
}
// TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.
//
//nolint:gocyclo
func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
bodyJSON := bodySignTOTPRequest{}
var (
userSession session.UserSession
config *model.TOTPConfiguration
valid, exists bool
step uint64
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication: %s", errStrUserSessionData)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if userSession.IsAnonymous() {
ctx.Logger.WithError(errUserAnonymous).Error("Error occurred validating a TOTP authentication")
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': %s", userSession.Username, errStrReqBodyParse)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if n := len(bodyJSON.Token); n != 6 && n != 8 {
ctx.Logger.Errorf("Error occurred validating a TOTP authentication for user '%s': expected code length is 6 or 8 but the user provided code was %d characters in length", userSession.Username, n)
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if config, err = ctx.Providers.StorageProvider.LoadTOTPConfiguration(ctx, userSession.Username); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred retreiving the configuration from the storage backend", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if valid, step, err = ctx.Providers.TOTP.Validate(ctx, bodyJSON.Token, config); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred validating the user input", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if !valid {
ctx.Logger.WithError(fmt.Errorf("the user input wasn't valid")).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred validating the user input", userSession.Username)
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeTOTP, nil)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if exists, err = ctx.Providers.StorageProvider.ExistsTOTPHistory(ctx, userSession.Username, step*uint64(config.Period)); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred checking the TOTP history", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if exists {
if ctx.Configuration.TOTP.DisableReuseSecurityPolicy {
ctx.Logger.WithFields(map[string]any{"username": userSession.Username}).Warn("User has reused a Time-based One Time Password with the given step but the policy to disable reuse is disabled")
} else {
ctx.Logger.WithError(fmt.Errorf("the user has already used this code recently and will not be permitted to reuse it")).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred satisfying security policies", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
} else if err = ctx.Providers.StorageProvider.SaveTOTPHistory(ctx, userSession.Username, step*uint64(config.Period)); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred saving the TOTP history to the storage backend", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeTOTP, nil); err != nil {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if err = ctx.RegenerateSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error regenerating the user session", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
config.UpdateSignInInfo(ctx.Clock.Now())
if err = ctx.Providers.StorageProvider.UpdateTOTPConfigurationSignIn(ctx, config.ID, config.LastUsedAt); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': error occurred saving the credential sign-in information to the storage backend", userSession.Username)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
userSession.SetTwoFactorTOTP(ctx.Clock.Now())
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred validating a TOTP authentication for user '%s': %s", userSession.Username, errStrUserSessionDataSave)
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetJSONError(messageMFAValidationFailed)
return
}
if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
} else {
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
}