authelia/internal/handlers/handler_user_session_elevat...

189 lines
5.1 KiB
Go

package handlers
import (
"database/sql"
"fmt"
"net/mail"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/templates"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/session"
)
func UserSessionElevateGET(ctx *middlewares.AutheliaCtx) {
var (
userSession session.UserSession
id uuid.UUID
key []byte
err error
)
if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
logger := ctx.Logger.WithFields(map[string]any{"username": userSession.Username})
if len(userSession.Emails) == 0 {
logger.WithError(fmt.Errorf("user has no registered emails")).Error("Error sending One Time Password to user")
ctx.ReplyForbidden()
return
}
if id, err = uuid.NewRandom(); err != nil {
logger.WithError(err).Error("Error occurred generating UUIDv4 public identifier for One Time Password")
ctx.ReplyForbidden()
return
}
if key, err = ctx.Providers.Random.BytesCustomErr(ctx.Configuration.IdentityValidation.SessionElevation.OTPCharacters, []byte(random.CharSetUnambiguousUpper)); err != nil {
logger.WithError(err).Error("Error occurred generating One Time Password")
ctx.ReplyForbidden()
return
}
if _, err = ctx.Providers.StorageProvider.SaveOneTimePassword(ctx, model.NewOneTimePassword(id, userSession.Username, model.OTPIntentElevateUserSession, ctx.Clock.Now(), ctx.Configuration.IdentityValidation.SessionElevation.ElevationExpiration, ctx.RemoteIP(), key)); err != nil {
logger.WithError(err).Error("Error occurred saving One Time Password to database")
ctx.ReplyForbidden()
return
}
data := templates.EmailOneTimePasswordValues{
Title: "Session Elevation",
DisplayName: userSession.DisplayName,
RemoteIP: ctx.RemoteIP().String(),
Identifier: id.String(),
OneTimePassword: string(key),
}
recipient := mail.Address{Name: userSession.DisplayName, Address: userSession.Emails[0]}
if err = ctx.Providers.Notifier.Send(ctx, recipient, "Session Elevation", ctx.Providers.Templates.GetOneTimePasswordEmailTemplate(), data); err != nil {
logger.WithError(err).Error("Error occurred sending One Time Password to email server")
ctx.ReplyForbidden()
return
}
ctx.ReplyOK()
}
func UserSessionElevatePOST(ctx *middlewares.AutheliaCtx) {
var (
body bodyUserSessionElevateRequest
provider *session.Session
userSession session.UserSession
otp *model.OneTimePassword
err error
)
if err = ctx.ParseBody(&body); err != nil {
ctx.Error(fmt.Errorf("unable to parse body for one time password: %w", err), messageOperationFailed)
return
}
if provider, err = ctx.GetSessionProvider(); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving session provider")
ctx.ReplyForbidden()
return
}
if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil {
ctx.Logger.WithError(err).Error("Error occurred retrieving user session")
ctx.ReplyForbidden()
return
}
logger := ctx.Logger.WithFields(map[string]any{"username": userSession.Username})
if otp, err = ctx.Providers.StorageProvider.LoadOneTimePassword(ctx, userSession.Username, model.OTPIntentElevateUserSession, body.OneTimePassword); err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
logger.Error("No One Time Password with the provided value matched the expected signature")
default:
logger.WithError(err).Error("Error occurred looking up One Time password from the database")
}
ctx.ReplyForbidden()
return
}
if otp.Consumed.Valid {
logger.Error("The provided One Time Password was already consumed")
ctx.ReplyForbidden()
return
}
if otp.Revoked.Valid {
logger.Error("The provided One Time Password was revoked")
ctx.ReplyForbidden()
return
}
if !otp.IssuedIP.IP.Equal(ctx.RemoteIP()) {
logger.WithFields(map[string]any{"issued_ip": otp.IssuedIP.IP.String()}).Error("The provided One Time Password was not issued to a user from that IP address and it will be revoked")
ctx.ReplyForbidden()
if err = ctx.Providers.StorageProvider.RevokeOneTimePassword(ctx, otp.PublicID, model.NewIP(ctx.RemoteIP())); err != nil {
logger.WithError(err).Error("Failed to revoke One Time Password")
}
return
}
otp.Consume(ctx.Clock.Now(), ctx.RemoteIP())
if err = ctx.Providers.StorageProvider.ConsumeOneTimePassword(ctx, otp); err != nil {
logger.WithError(err).Error("Failed to consume One Time Password")
ctx.ReplyForbidden()
return
}
userSession.Elevations.User = &session.Elevation{
ID: otp.ID,
RemoteIP: ctx.RemoteIP(),
Expires: ctx.Clock.Now().Add(ctx.Configuration.IdentityValidation.SessionElevation.ElevationExpiration),
}
if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
logger.WithError(err).Error("Failed to save session for elevation")
ctx.ReplyForbidden()
return
}
ctx.ReplyOK()
}