draincloud-core/internal/app/handlers/logon.go

180 lines
5.0 KiB
Go
Raw Normal View History

package handlers
2024-11-23 08:52:06 +00:00
import (
"bytes"
"context"
"encoding/json"
2024-11-23 08:52:06 +00:00
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
2024-11-23 08:52:06 +00:00
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
2024-11-23 08:52:06 +00:00
"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
2025-01-10 23:09:37 +00:00
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models/auth"
2024-12-16 05:08:37 +00:00
"github.com/google/uuid"
2024-11-23 08:52:06 +00:00
"golang.org/x/crypto/bcrypt"
)
type LogonHandler struct {
*handler.BaseHandler
authStorage storage.AuthStorage
}
2024-11-23 08:52:06 +00:00
func NewLogonHandler(
authStorage storage.AuthStorage,
) *LogonHandler {
h := &LogonHandler{
authStorage: authStorage,
BaseHandler: handler.New().
WithName("logonv1").
WithRequiredResolveParams(),
}
h.WithProcessFunc(h.process)
return h
}
2024-11-23 08:52:06 +00:00
func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error {
logger.Debug(ctx, "[Logon] new request")
2024-11-23 08:52:06 +00:00
body := new(domain.LogonRequest)
err := json.Unmarshal(req.Body, body)
2024-11-23 08:52:06 +00:00
if err != nil {
logger.Error(ctx, "[Logon] failed to bind request", logger.Err(err))
w.Write(ctx, map[string]string{
"error": "bad request",
}, handler.WithCode(http.StatusBadRequest))
return nil
2024-11-23 08:52:06 +00:00
}
session, err := h.getSession(ctx, req)
2024-11-23 08:52:06 +00:00
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return err
2024-11-23 08:52:06 +00:00
}
if session != nil {
2025-01-10 23:09:37 +00:00
if err := validateSession(req, session); err != nil {
2024-11-23 08:52:06 +00:00
// TODO add audit log entry
return err
2024-11-23 08:52:06 +00:00
}
2024-12-16 05:08:37 +00:00
logger.Debug(ctx, "[login] user is already logged in", slog.String("session_id", session.ID.String()))
w.Write(ctx, &domain.LogonResponse{
2024-11-23 08:52:06 +00:00
Ok: true,
})
return nil
2024-11-23 08:52:06 +00:00
}
logger.Debug(ctx, "[login] session not founh. trying to authorize")
resp, err := h.login(ctx, body, session, w)
if err != nil {
logger.Error(ctx, "[Logon] failed to login user", logger.Err(err))
return err
}
w.Write(ctx, resp)
return nil
}
2024-11-23 08:52:06 +00:00
2025-01-10 23:09:37 +00:00
func (h *LogonHandler) login(ctx context.Context, req *domain.LogonRequest, session *auth.Session, w handler.Writer) (*domain.LogonResponse, error) {
2024-11-23 08:52:06 +00:00
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil {
logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err))
return nil, fmt.Errorf("failed to generate password hash: %w", err)
}
user, err := h.authStorage.GetUserByLogin(ctx, req.Login)
2024-11-23 08:52:06 +00:00
if err != nil {
return nil, fmt.Errorf("failed to fetch user by login: %w", err)
}
if bytes.Equal(passwordHash, user.PasswordHash) {
logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal")
return nil, errs.ErrorAccessDenied
2024-11-23 08:52:06 +00:00
}
sessionCreatedAt := time.Now()
sessionExpiredAt := sessionCreatedAt.Add(time.Hour * 24 * 7)
sessionToken, err := generateSessionToken(100)
if err != nil {
return nil, fmt.Errorf("failed to generate a session token: %w", err)
}
w.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true)
2024-11-23 08:52:06 +00:00
csrfToken, err := generateSessionToken(100)
if err != nil {
return nil, fmt.Errorf("failed to generate a csrf token: %w", err)
}
w.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false)
2024-11-23 08:52:06 +00:00
2024-12-16 05:08:37 +00:00
sessionID, err := uuid.NewV7()
if err != nil {
return nil, fmt.Errorf("failed to generate session id: %w", err)
}
2025-01-10 23:09:37 +00:00
if _, err = h.authStorage.AddSession(ctx, &auth.Session{
2024-12-16 05:08:37 +00:00
ID: sessionID,
2024-11-23 08:52:06 +00:00
SessionToken: sessionToken,
CsrfToken: csrfToken,
UserID: user.ID,
CreatedAt: sessionCreatedAt,
ExpiredAt: sessionExpiredAt,
}); err != nil {
return nil, fmt.Errorf("failed to save session: %w", err)
}
// TODO add audit log entry
return &domain.LogonResponse{
2024-11-23 08:52:06 +00:00
Ok: true,
}, nil
}
2025-01-10 23:09:37 +00:00
func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*auth.Session, error) {
token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
2024-11-23 08:52:06 +00:00
if err != nil {
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
}
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
2024-11-23 08:52:06 +00:00
if err != nil {
return nil, fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
}
if len(csrfToken) == 0 || len(token) == 0 {
return nil, fmt.Errorf("session token or csrf token is empty")
}
session, err := h.authStorage.GetSession(ctx, token)
2024-11-23 08:52:06 +00:00
if err != nil {
return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
}
return session, nil
}
2025-01-10 23:09:37 +00:00
func validateSession(req *common.Request, session *auth.Session) error {
2024-11-23 08:52:06 +00:00
if session == nil {
return errs.ErrorAccessDenied
2024-11-23 08:52:06 +00:00
}
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
2024-11-23 08:52:06 +00:00
if err != nil {
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
}
if session.CsrfToken != csrfToken {
return errs.ErrorAccessDenied
2024-11-23 08:52:06 +00:00
}
if session.ExpiredAt.Before(time.Now()) {
return errs.ErrorSessionExpired
2024-11-23 08:52:06 +00:00
}
return nil
}