draincloud-core/internal/app/login.go

157 lines
4.2 KiB
Go
Raw Normal View History

2024-11-23 08:52:06 +00:00
package app
import (
"bytes"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
var (
ErrorAccessDenied = errors.New("access denied")
ErrorSessionExpired = errors.New("session expired")
)
func (d *DrainCloud) Login(ctx *gin.Context) {
logger.Debug(ctx, "[Login] new request")
req := new(domain.LoginRequest)
err := ctx.BindJSON(req)
if err != nil {
logger.Error(ctx, "[Login] failed to bind request", logger.Err(err))
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "bad request",
})
return
}
resp, err := d.login(ctx, req)
if err != nil {
logger.Error(ctx, "[Login] failed to login user", logger.Err(err))
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, resp)
}
func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain.LoginResponse, error) {
session, err := d.getSession(ctx)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return nil, err
}
if session != nil {
if err := validateSession(ctx, session); err != nil {
// TODO add audit log entry
return nil, err
}
logger.Debug(ctx, "[login] user is already logged in", slog.Int64("session_id", session.ID))
return &domain.LoginResponse{
Ok: true,
}, nil
}
logger.Debug(ctx, "[login] session not found. trying to authorize")
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 := d.database.GetUserByLogin(ctx, req.Login)
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, ErrorAccessDenied
}
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)
}
ctx.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true)
csrfToken, err := generateSessionToken(100)
if err != nil {
return nil, fmt.Errorf("failed to generate a csrf token: %w", err)
}
ctx.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false)
if _, err = d.database.AddSession(ctx, &models.Session{
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.LoginResponse{
Ok: true,
}, nil
}
func (d *DrainCloud) getSession(ctx *gin.Context) (*models.Session, error) {
token, err := ctx.Cookie(sessionTokenCookie)
if err != nil {
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
}
csrfToken, err := ctx.Cookie(csrfTokenCookie)
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 := d.database.GetSession(ctx, token)
if err != nil {
return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
}
return session, nil
}
func validateSession(ctx *gin.Context, session *models.Session) error {
if session == nil {
return ErrorAccessDenied
}
csrfToken, err := ctx.Cookie(csrfTokenCookie)
if err != nil {
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
}
if session.CsrfToken != csrfToken {
return ErrorAccessDenied
}
if session.ExpiredAt.Before(time.Now()) {
return ErrorSessionExpired
}
return nil
}